Compare commits

...
21 Commits
Author SHA1 Message Date
Tunglies 39ba1e64a6 chore(deps): remove unused dependencies (#8559)
- Drop tauri-plugin-process: no frontend or Rust consumer; exit and
  restart already go through the exit_app/restart_app commands backed
  by AppHandle::exit/restart. Remove the registration and the unused
  process:allow-exit capability.
- Replace the glob dev dependency with Node's built-in fs.globSync in
  prebuild's service permission step (Node baseline is 26.13.0).
- Remove unused direct Cargo declarations: bitflags and regex (source
  and workspace entries), and src-tauri's criterion dev-dependency;
  clash-verge-draft keeps criterion for its bench.
- Narrow sysinfo to the network/system features actually consumed.
- Use futures::StreamExt for DelayQueue in the timer and drop the
  tokio-stream direct declaration (still transitively present via
  clash-verge-service-ipc).
2026-11-04 09:23:16 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 27da92b5ff chore(deps): bump the npm_and_yarn group across 1 directory with 3 updates (#8558)
Bumps the npm_and_yarn group with 3 updates in the / directory: [follow-redirects](https://github.com/follow-redirects/follow-redirects), [source-map-js](https://github.com/7rulnik/source-map-js) and [undici](https://github.com/nodejs/undici).


Updates `follow-redirects` from 1.16.0 to 1.16.2
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.16.0...v1.16.2)

Updates `source-map-js` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

Updates `undici` from 6.28.1 to 6.29.1
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.28.1...v6.29.1)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: undici
  dependency-version: 6.29.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-04 09:22:25 +00:00
Tunglies f076d4cd5d fix(macos): preserve DNS and proxy state across network changes 2026-11-04 11:19:16 +08:00
Tunglies 97dc1622ee chore(updater): prefer GitHub before mirror endpoints
Relate #8529
2026-11-04 11:19:16 +08:00
Tunglies f65a7f1348 fix(tray): refresh icon after service operations
Refresh the tray menu, icon, and tooltip after service operations so restored TUN capability is reflected without changing the saved TUN setting.

Ref: e60285b4ae
2026-11-04 11:19:16 +08:00
wonfen 14abc4c4ee refactor(react): replace avoidable effects with derived state and handlers
Also drops a duplicate WebDAV history request on open and the per-keystroke DNS YAML dump.
2026-11-04 10:17:52 +08:00
wonfen 84bbe790e8 fix(proxies): move an unpicked GLOBAL off DIRECT to its first group
Keep DIRECT and REJECT listed in global mode.
2026-11-04 10:17:52 +08:00
renovate[bot]Tungliesrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
e4d82a1b3f chore(deps): update tauri (#8388)
* chore(deps): update tauri

* fix(deps): restore compatible Specta and dialog dependencies

* fix(deps): align Specta release versions

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>

* fix(deps): regenerate Specta bindings and align consumers

* fix(deps): use the dialog plugin GTK backend

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-11-03 23:56:44 +08:00
renovate[bot]Tungliesrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
9fb46607b5 chore(deps): update cargo dependencies (#8384)
* fix(webdav): allow clearing saved configuration

Close #8495

* chore(deps): update cargo dependencies

* fix(ci): resolve Rust dependency update failures

* chore(deps): remove unrelated changelog entries

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-11-03 15:26:38 +00:00
wonfen 943e18e8ca fix(proxies): list only GLOBAL members in global mode
Reveal a group once GLOBAL selects it; hide DIRECT and REJECT unless selected.
2026-11-03 22:31:53 +08:00
Tunglies 8be05c178b fix(renovate): create Specta pull requests before CI 2026-11-03 22:28:56 +08:00
Tunglies 01fbc5714f style(renovate): format Specta package rules 2026-11-03 22:21:22 +08:00
Tunglies 5d41f99eb1 chore(renovate): group compatible Specta dependencies 2026-11-03 22:17:58 +08:00
Tunglies 1105eedeb6 fix(react): avoid redundant DNS and proxy updates (#8543) 2026-11-03 12:58:43 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 891757ede6 chore(deps): update dependency vite to v8.3.3 [security] (#8538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-11-03 12:36:14 +00:00
Tunglies 2b7b7cc0e7 fix(updater): refresh changed update offers before installation 2026-11-03 18:57:05 +08:00
Tunglies 40fd1d5012 refactor: frontend cleanup (#8447)
* docs(agents): add frontend liveness constraints

Add six liveness rules to the frontend guardrails covering dead state,
unused exposed surface, unreachable fallback paths, duplicate side
effects per interaction, unreachable mounts and unbounded resource
lifecycles. These target defect classes that the existing CI gates
(knip, eslint, arch:check) cannot detect mechanically.

* refactor(frontend): remove dead states, refs and unreachable branches

Drop state that can only hold its initial value (profiles page disabled
flag, traffic graph currentFPS), a write-only window controls ref with
its unconsumed imperative handle, a no-op appendData ref method, an
unreachable gradient branch, an invisible fadeIn shell, constant-true
subexpressions in the system info card, an empty click handler and a
pointless useMemo.

* refactor(frontend): drop unused params, props and api surface

Remove the never-read _revalidate argument of mutateVerge and its 16
call-site placeholders, seven never-passed props of the split chip
editor, the unconsumed refreshDecorated context member, the now-orphaned
toggleFullscreen control, the unused translate argument of the notice
message handler, the reserved-but-unset ServiceConfig.timeout field and
unreachable branches in parseNoticeExtras.

* refactor(types): remove dead provider view types superseded by bindings

Drop the handwritten IProxyProviderItem and IRuleProviderItem
interfaces: IProxyProviderItem is a stale duplicate of the generated
ProxyProviderView contract, and neither has any references left in
the tree.

Keep IProxyItem and IProxyGroupItem: IConfigData still declares
'proxy-groups'?: IProxyGroupItem[], which builds on IProxyItem.

* fix(rules): refresh once per visibility trigger

The mount effect fired refreshRules and refreshRuleProviders
unconditionally and again under the page-visible flag, double-firing
both queries on mount and on every hide-to-show transition. Gate the
refresh on visibility once so each trigger performs exactly one
revalidation.

* fix(setting): write auto launch patch once per toggle

GuardState applies the optimistic onChange write before onGuard runs,
so repeating the same onChangeData patch inside the guard handler
duplicated every cache write and subscriber notification for the auto
launch switch.

* fix(setting): build viewer form defaults from a single source

sysproxy, misc and tunnels viewers duplicated their form default
literals in useState and open(), and the two copies in misc-viewer had
already diverged (app log max size, app log max count, auto log clean).
Extract a buildInitialValues factory per viewer and unify on the open()
values, which are the only ones users could ever see.

* fix(setting): mount only reachable proxy control viewers

ProxyControlSwitches unconditionally mounted both SysproxyViewer and
TunViewer while its switch rows are mutually exclusive, so every
instance carried a dialog that could never open, each firing
getEmbeddedServerPort or a clash query subscription on mount. Mount
each viewer under the same condition as the switch row that opens it.

SettingSystem also mounted its own SysproxyViewer and TunViewer whose
refs were never opened there; the reachable entries are the
ProxyControlSwitches rows, so the dead pair is removed. Viewer mounts
across the app drop from eight (setting-system: two own + two per
instance, proxy-tun-card: two) to three (one per rendered switch
variant).

* fix(delay): write pending state once per node in batch checks

checkListDelay pre-marked every node as checking, help() re-marked it
when picked up and measureDelay marked it a third time on entry, so
each batch run queued three identical pending updates per node. Keep
the pre-mark (immediate feedback for queued nodes) and drop the
redundant marks along the chain.

* fix(services): make retries viable and align polling freshness window

Share no abort signal across retry attempts: after a timeout abort the
old signal stays dead and every later fetch rejects immediately, so
retries only burned backoff time. Also drop the system state query's
staleTime: the wrapper maps it to SWR's dedupingInterval, which
swallowed every other 3s poll tick while it exceeded the interval.

* refactor(traffic): drop unreachable inline monitor fallback

The inline traffic monitor duplicated the worker's message loop,
snapshot throttling and aging logic but only runs when Worker is
undefined or its constructor throws, which no supported Tauri webview
or browser hits. Keep the worker as the single implementation and
remove the mode switch and the pending-message queue that could only
ever buffer the init message.

* refactor(shared): replace the traffic error boundary with ErrorBoundary

TrafficErrorBoundary maintained a second boundary system beside
BaseErrorBoundary: a report builder that only console.logged constants
in the Tauri webview, an onError that re-logged what componentDidCatch
already logged, and a fallback wrapper with a single consumer. Fold
both mount points into react-error-boundary (the primitive behind
BaseErrorBoundary) with local fallbacks that keep the panel styling,
message text and retry reset, log each error once, and drop the retry
attempt cap and the stack-details collapse along with it.

* refactor(hooks): drop the write-only error channel and key-rotating refresh

The mihomo ws subscription's next callback accepted an error half that
baseNext silently discarded while traffic, memory and log consumers
kept constructing fallback payloads for it. Refresh minted a fresh
SWR key per call via a localStorage timestamp, abandoning old keys in
the cache forever. Reduce next to its data half and reset the snapshot
under a stable key instead.

* refactor(hooks): trim dead surface from proxy selection

Drop the connection-cleanup option nobody ever disabled (behavior now
unconditional), the returned changeProxy no consumer called and the two
one-caller argument adapters; both call sites use the shared select
entry directly.

* refactor(services): drop the bespoke retry engine from the query wrapper

The custom onErrorRetry existed only to honor a function-form
retryDelay, which exactly one caller (the app data provider) used for
exponential backoff. Fall back to SWR's built-in error retry and
express that caller's cadence with constant options.
2026-11-03 10:48:45 +00:00
Tunglies 8039feb270 fix: backend lock safety (#8439)
* fix(backup): serialize restore with config write lock

Acquire the configuration write lock at the entry of
finalize_restored_verge_config so the clash, profiles and verge draft
applies all happen under it, instead of only the verge layer. A
concurrent patch (patch_clash / patch_verge via try_lock_config_write)
could otherwise stage a draft that the unlocked restore applies
overwrite, or interleave its persistence with the restore.

Also drop the pointless clones in the clash and profiles edit_draft
closures; edit_draft takes FnOnce so the restored values can be moved
in. The lock is released before patch_verge, which re-acquires it via
try_lock_config_write.

* perf(backup): extract archive off the runtime

Run File::open, ZipArchive::new and the extraction of the backup
archive inside spawn_blocking. Extracting a multi-file backup blocked
a tokio worker for hundreds of milliseconds to seconds while the
runtime kept serving other work on the remaining workers.

Move the PROFILE_WRITE_LOCK acquisition after the extraction, right
before finalize_restored_verge_config: the lock protects the
config-data transition (extracted files reloaded into drafts), not the
plain filesystem writes, so profile operations no longer queue behind
the blocking unzip.

* fix(clash): take config write lock when changing mode

change_clash_mode committed the clash draft and saved config.yaml without
claiming CONFIG_WRITE_LOCK, so a concurrent patch_clash transaction could
have its staged draft committed here or its file-snapshot rollback swallow
the mode change. Take lock_config_write (waiting) rather than
try_lock_config_write: two of the three callers (hotkey, tray) discard the
result, so fail-fast would silently drop the switch, and the lock is only
attempted after the mihomo patch already applied, where an Err would roll
the frontend back while the core runs the new mode.
2026-11-03 10:48:44 +00:00
Tunglies 9a904af3aa refactor: backend cleanup (#8436)
* refactor(utils): drop dead reset helper and tidy init_resources

- remove resolve_reset_async, which had no callers
- drop the duplicated time.*.com entry from the default fake-ip-filter
- init_resources no longer recreates app_home (ensure_directories during
  init_config already created it) and now propagates res_dir creation errors

* refactor(profile): remove identical blind retry when switching node

The second select_node_for_group call in switch_proxy_node was a
zero-interval retry with identical arguments. Its success path was a
degraded copy of the first (missing the Refresh::Proxies announcement
ever since the old force-refresh emit was removed), and its distinct
"回退/最终失败" log wording misled maintainers. Keep a single call,
the primary success aftermath, and one error log.

* refactor(enhance): simplify tun mode check and lowercase helpers

- Drop the always-true second clause in use_tun's enhanced-mode check:
  the key is absent only when the unwrap_or default already makes
  current_mode == "fake-ip" true.
- Collapse use_lowercase into a delegation to use_lowercase_owned.

* refactor(core): drop unreachable hotkey dtor and dedupe tray predicate

Remove impl Drop for Hotkey: the instance lives in a OnceLock static via
the singleton! macro and is never dropped; exit-time unregistration is
already handled by Hotkey::reset().

Spell the include_proxy_groups/display-mode predicate identically at
both evaluation sites in tray menu building; behavior is unchanged
(Option != Some("disable") is equivalent to unwrap_or("default") != "disable").

* refactor(config): fold port guards into single helper

The five IClashTemp port guards duplicated the same read/parse/default
logic with drifting defaults: some hardcoded 7895/7898/7899 while others
referenced network::ports constants. Route them all through one
guard_port_value helper and use the constants everywhere, keeping each
guard's name, key and cfg attributes.

* refactor(config): dedupe prfitem factories and companion seeding

from_rules/from_proxies/from_groups differed only in uid prefix, type
string and template, so they now delegate to a shared from_seq_item.
The five current_* accessors collapse into a local macro. The identical
five-block companion seeding in from_local and from_url moves into
ensure_companion_items, preserving seeding order and error propagation.

* refactor(config): share patch macro and profiles helpers

IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each
hand-rolled the same copy-Some-overwrite pattern; they now share one
patch_opt_field macro (the or-else merge form is field-for-field
equivalent for Option fields). The four profiles *_safe wrappers that
inlined Config::profiles().with_data_modify boilerplate delegate to a
with_profiles_edit helper, and record/forget_selected_node share the
current-item lookup prelude via current_item_mut.

* refactor(enhance): merge chain processing and drop unused builtin js payload

- collapse the identical process_global_items/process_profile_items into
  one parameterized process_override_items (entry uid sources unchanged)
- fold the identical rules/proxies/groups arms of ChainItem::from_async
  into a single arm with an inner dispatch
- stop embedding builtin JS payloads never executed by any path: the
  enhance loop dispatches builtin items by uid to native implementations,
  so ChainItem::builtin now carries empty script data (uids unchanged)
- source ProfileItems fallback literals from Default instead of a second
  hand-written copy in collect_profile_items

* refactor(net): reuse webpki tls config and fallback

Extract build_webpki_rooted_tls_config so the delay-test TLS connector
and the reqwest clients share one static-webpki-roots builder; ALPN
stays caller-specific (only reqwest offers h2).

Collapse the duplicated platform-verifier -> static-webpki-roots retry
in get/get_bytes into request_with_static_webpki_fallback, gated by the
caller's certificate-verification setting.

* refactor(app): extract common exit cleanup

restart_app and quit duplicate the whole exit preparation: set the
exiting flag, persist configs, run interactive cleanup, and on a failed
core stop clear the flag, restore public DNS under the lifecycle lock
on macOS, and send the stop-failed notice.

Move that flow into run_exit_cleanup_flow next to clean_async, taking
the notice variant as a parameter and returning the CleanupResult so
each caller keeps its own tail: restart aborts before shutdown, quit
always shuts down and exits with the cleanup code.

* refactor(backup): share restore finalize path

restore_webdav_backup and restore_local_backup duplicated the
credential extraction (backup creation strips the WebDAV credentials,
so both re-inject the live ones), the archive extraction and the
finalize tail. Move that tail into a private restore_backup_archive
helper so the two entries keep only their download/locate logic; the
config-write lock scope and the off-runtime extraction land in one
place. The webdav entry keeps its downloaded-file cleanup.

* refactor(sysopt): drop redundant lock and busy-wait

The proxy target pair sat behind an Arc<RwLock> with no readers and an
Arc that was never cloned; both writers already serialize on
update_lock, so store the pair inside that mutex and delete the RwLock.

refresh_guard waited for the guard to become ready with a yield_now spin
at full CPU while holding guard_operation_lock; a 10 ms sleep keeps the
same wait condition and lock scope without the spin.

* refactor(sysopt): share macos bypass formatting

The macOS default bypass list and its three-way composition (empty ->
default, default + custom, custom only) were maintained twice: in
sysopt's format_bypass and inline in proxy_control's service_bypass.
Keep the literal once in constants and compose both paths through
format_bypass; service_bypass keeps its NUL check and UTF-8 truncation.

* refactor(core): dedupe window listeners, elevated runners, backup settings

- Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label.
- Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms.
- Make the auto-backup watch channel the single settings source, replacing the duplicate Arc<RwLock> copy with a stored receiver.

* refactor(enhance): fold lowercase helpers into single owned function

use_lowercase_owned disappears; the surviving use_lowercase takes the
owned Mapping and borrowed callers clone at the call site.

* refactor(enhance): rename ChainSupport variants to Mihomo names

Clash is a legacy name; the variants match the verge-mihomo and
verge-mihomo-alpha cores they gate.

* refactor(enhance): lowercase takes a reference and chain support is copy

use_lowercase borrows the mapping and clones only the values; call
sites no longer clone whole configs. ChainSupport is a fieldless enum
and now Copy; builtin() registers each support pair without clones.

* refactor(config): name prfitem constructor for what it builds

new_seq_item states that a fresh item is built from a template; the
from_ prefix wrongly implied an existing source. append_companion_item
returns the uid directly: append_item bails on a missing uid, so the
Option was always Some on the Ok path.

* refactor(sysopt): name the proxy state lock for its full role

The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock
says what it guards; the guard locals follow. constants::proxy held a
single sysproxy constant and is renamed sysproxy.

* refactor: replace remaining clash meta naming with mihomo

Doc comment, generated bindings and the prebuild sidecar helpers stop
referring to the legacy Clash Meta name; installer legacy-process
cleanup keeps the old executable names on purpose.

* refactor(enhance): drop builtin core filtering that never discriminates

Both valid cores matched a different ChainSupport variant, yet each
variant registered the same uid pair, so the filtered builtin list was
identical for stable and alpha. Invalid cores are coerced at startup
(validate_and_fix_config / get_valid_clash_core) and the None arm of
is_support was unreachable. builtin() now returns the items directly
and the clash_core threading through ConfigValues goes away; re-add
core discrimination when a builtin actually differs per core.

* refactor(enhance): drop unreachable mode script guard

merge_default_config overwrites the profile's mode with the verge-owned
clash config, whose mode is always present (template seeds rule and
patch semantics never clear it) and restricted to rule/global/direct;
change_clash_mode is rejected by the core before persisting anything
else. mode:script can therefore only arrive through a hand-crafted
patch_clash call, and for that path an explicit invalid-mode validation
error is more honest than silently rewriting to rule. mihomo rejects
mode:script outright (invalid mode), and user merge/script chains run
after the builtins, so the guard never covered them anyway.

* refactor(enhance): call the hysteria alpn builtin directly

builtin() existed to feed the uid-dispatch loop; with a single
builtin the vec and match are ceremony. apply_builtin_scripts calls
builtin_hy_alpn directly and to_script goes away; tests construct
ChainItem literals the way the merge items already did.
2026-11-03 10:48:44 +00:00
github-actions[bot]andgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> d7964e6d80 Dev: v2.6.0 (#8537)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-11-03 10:07:50 +00:00
Tunglies 6aeae2f970 fix(ci): share build caches and mention the release team 2026-11-03 17:57:22 +08:00
126 changed files with 2438 additions and 3365 deletions
+8 -53
View File
@@ -157,15 +157,12 @@ jobs:
toolchain: '1.99.1' toolchain: '1.99.1'
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
@@ -188,14 +185,6 @@ jobs:
node-version: '26.13.0' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
@@ -204,12 +193,6 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version - name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest run: pnpm release-version autobuild-latest
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build for Windows-macOS-Linux - name: Tauri build for Windows-macOS-Linux
uses: tauri-apps/tauri-action@v1 uses: tauri-apps/tauri-action@v1
env: env:
@@ -260,15 +243,12 @@ jobs:
toolchain: '1.99.1' toolchain: '1.99.1'
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
@@ -285,14 +265,6 @@ jobs:
node-version: '26.13.0' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
@@ -345,12 +317,6 @@ jobs:
gcc-arm-linux-gnueabihf \ gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf g++-arm-linux-gnueabihf
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri Build for Linux - name: Tauri Build for Linux
run: | run: |
export PKG_CONFIG_ALLOW_CROSS=1 export PKG_CONFIG_ALLOW_CROSS=1
@@ -404,15 +370,18 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Add Rust Target - name: Install Rust Stable
run: rustup target add ${{ matrix.target }} uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}-fixed-webview2'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
@@ -429,14 +398,6 @@ jobs:
node-version: '26.13.0' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
@@ -452,12 +413,6 @@ jobs:
Remove-Item .\src-tauri\tauri.windows.conf.json Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build for Windows - name: Tauri build for Windows
id: build id: build
uses: tauri-apps/tauri-action@v1 uses: tauri-apps/tauri-action@v1
+2 -10
View File
@@ -77,11 +77,12 @@ jobs:
uses: dtolnay/rust-toolchain@1.99.1 uses: dtolnay/rust-toolchain@1.99.1
- name: Rust Cache - name: Rust Cache
if: github.event.inputs[matrix.input] == 'true'
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
@@ -106,15 +107,6 @@ jobs:
node-version: '26.13.0' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Pnpm Cache
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: 'pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
restore-keys: |
pnpm-shared-stable-${{ matrix.os }}-${{ matrix.target }}
lookup-only: true
- name: Pnpm install and check - name: Pnpm install and check
if: github.event.inputs[matrix.input] == 'true' if: github.event.inputs[matrix.input] == 'true'
run: | run: |
-8
View File
@@ -39,14 +39,6 @@ jobs:
node-version: '26.13.0' node-version: '26.13.0'
cache: 'pnpm' cache: 'pnpm'
- name: Restore pnpm cache
uses: actions/cache@v6
with:
path: ~/.pnpm-store
key: "pnpm-shared-stable-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}"
restore-keys: |
pnpm-shared-stable-${{ runner.os }}-
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Run Prettier check - name: Run Prettier check
+21 -45
View File
@@ -206,15 +206,12 @@ jobs:
toolchain: '1.99.1' toolchain: '1.99.1'
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
@@ -226,27 +223,22 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
pnpm run prebuild ${{ matrix.target }} pnpm run prebuild ${{ matrix.target }}
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v1.0.0 uses: tauri-apps/tauri-action@v1.0.0
env: env:
@@ -319,30 +311,28 @@ jobs:
toolchain: '1.99.1' toolchain: '1.99.1'
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
cache-workspace-crates: true cache-workspace-crates: true
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
with: with:
run_install: false run_install: false
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
@@ -392,12 +382,6 @@ jobs:
gcc-arm-linux-gnueabihf \ gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf g++-arm-linux-gnueabihf
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Build for Linux - name: Build for Linux
run: | run: |
export PKG_CONFIG_ALLOW_CROSS=1 export PKG_CONFIG_ALLOW_CROSS=1
@@ -458,30 +442,28 @@ jobs:
toolchain: '1.99.1' toolchain: '1.99.1'
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
with: with:
save-if: ${{ github.ref == 'refs/heads/dev' }} save-if: ${{ github.ref == 'refs/heads/dev' }}
prefix-key: 'v1-rust' prefix-key: 'v1-rust'
key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}' shared-key: 'rust-shared-stable-${{ matrix.os }}-${{ matrix.target }}-fixed-webview2'
workspaces: | workspaces: |
. -> target . -> target
cache-all-crates: true cache-all-crates: true
cache-workspace-crates: true cache-workspace-crates: true
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
- uses: pnpm/action-setup@v6 - uses: pnpm/action-setup@v6
name: Install pnpm name: Install pnpm
with: with:
run_install: false run_install: false
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check - name: Pnpm install and check
run: | run: |
pnpm i pnpm i
@@ -494,12 +476,6 @@ jobs:
Remove-Item .\src-tauri\tauri.windows.conf.json Remove-Item .\src-tauri\tauri.windows.conf.json
Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json Rename-Item .\src-tauri\webview2.${{ matrix.arch }}.json tauri.windows.conf.json
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v1.0.0 uses: tauri-apps/tauri-action@v1.0.0
env: env:
Generated
+189 -153
View File
@@ -150,7 +150,7 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
"parking_lot", "parking_lot 0.12.5",
"percent-encoding", "percent-encoding",
"windows-sys 0.60.2", "windows-sys 0.60.2",
"wl-clipboard-rs", "wl-clipboard-rs",
@@ -159,9 +159,9 @@ dependencies = [
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.9.2" version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"
dependencies = [ dependencies = [
"rustversion", "rustversion",
] ]
@@ -1081,7 +1081,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]] [[package]]
name = "clash-verge" name = "clash-verge"
version = "2.5.10" version = "2.6.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"anyhow", "anyhow",
@@ -1089,7 +1089,6 @@ dependencies = [
"async-trait", "async-trait",
"backon", "backon",
"base64 0.23.1", "base64 0.23.1",
"bitflags 2.13.2",
"boa_engine", "boa_engine",
"bytes", "bytes",
"chrono", "chrono",
@@ -1100,7 +1099,6 @@ dependencies = [
"clash-verge-media-unlock", "clash-verge-media-unlock",
"clash-verge-signal", "clash-verge-signal",
"clash_verge_service_ipc", "clash_verge_service_ipc",
"criterion",
"dark-light", "dark-light",
"deelevate", "deelevate",
"dirs 7.0.0", "dirs 7.0.0",
@@ -1111,16 +1109,14 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"libc", "libc",
"log", "log",
"minisign-verify",
"nanoid", "nanoid",
"network-interface", "network-interface",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"open", "open",
"parking_lot", "parking_lot 0.13.0",
"percent-encoding", "percent-encoding",
"regex",
"reqwest 0.13.5", "reqwest 0.13.5",
"reqwest_dav", "reqwest_dav",
"rfd", "rfd",
@@ -1150,7 +1146,6 @@ dependencies = [
"tauri-plugin-mihomo", "tauri-plugin-mihomo",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-opener", "tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater", "tauri-plugin-updater",
"tauri-plugin-wdio-webdriver", "tauri-plugin-wdio-webdriver",
@@ -1158,7 +1153,6 @@ dependencies = [
"tauri-specta", "tauri-specta",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-stream",
"tokio-util", "tokio-util",
"tracing", "tracing",
"warp", "warp",
@@ -1176,7 +1170,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"criterion", "criterion",
"parking_lot", "parking_lot 0.13.0",
"tokio", "tokio",
] ]
@@ -1199,7 +1193,7 @@ dependencies = [
"anyhow", "anyhow",
"flexi_logger", "flexi_logger",
"log", "log",
"parking_lot", "parking_lot 0.13.0",
"tracing-estuary", "tracing-estuary",
] ]
@@ -1256,8 +1250,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66" checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"lock_api", "lock_api 0.4.14",
"parking_lot_core", "parking_lot_core 0.9.12",
"polonius-the-crab", "polonius-the-crab",
] ]
@@ -1334,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1768,8 +1762,18 @@ version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.23.0",
"darling_macro", "darling_macro 0.23.0",
]
[[package]]
name = "darling"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
"darling_core 0.24.1",
"darling_macro 0.24.1",
] ]
[[package]] [[package]]
@@ -1785,17 +1789,41 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "darling_core"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 3.0.5",
]
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.23.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [ dependencies = [
"darling_core", "darling_core 0.23.0",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "darling_macro"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core 0.24.1",
"quote",
"syn 3.0.5",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.2.1" version = "6.2.1"
@@ -1805,9 +1833,9 @@ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
"hashbrown 0.14.5", "hashbrown 0.14.5",
"lock_api", "lock_api 0.4.14",
"once_cell", "once_cell",
"parking_lot_core", "parking_lot_core 0.9.12",
] ]
[[package]] [[package]]
@@ -1885,9 +1913,9 @@ dependencies = [
[[package]] [[package]]
name = "devtools-core" name = "devtools-core"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c53d416e473377276435d49d731a75d253b96ee4339831cd85653702b94d8298" checksum = "c9e85926bda763dd434fcfbcfb24af4b2c1a8c3e3c34bcd5d844c48f655cca51"
dependencies = [ dependencies = [
"bytes", "bytes",
"devtools-wire-format", "devtools-wire-format",
@@ -2015,7 +2043,7 @@ dependencies = [
"libc", "libc",
"option-ext", "option-ext",
"redox_users 0.5.2", "redox_users 0.5.2",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -2274,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -2790,7 +2818,7 @@ dependencies = [
"gobject-sys 0.21.5", "gobject-sys 0.21.5",
"libc", "libc",
"system-deps 7.0.8", "system-deps 7.0.8",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -3344,7 +3372,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.61.2", "windows-core 0.62.2",
] ]
[[package]] [[package]]
@@ -3358,12 +3386,12 @@ dependencies = [
[[package]] [[package]]
name = "ico" name = "ico"
version = "0.5.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"png 0.17.16", "png",
] ]
[[package]] [[package]]
@@ -3549,7 +3577,7 @@ dependencies = [
"byteorder-lite", "byteorder-lite",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png 0.18.1", "png",
"tiff", "tiff",
] ]
@@ -3858,7 +3886,7 @@ dependencies = [
"http", "http",
"httparse", "httparse",
"libc", "libc",
"parking_lot", "parking_lot 0.12.5",
"path-tree", "path-tree",
"pin-project-lite", "pin-project-lite",
"rand 0.10.2", "rand 0.10.2",
@@ -4019,10 +4047,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "log" name = "lock_api"
version = "0.4.34" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" checksum = "65d91b22da179613dcf4d6ec8349563f52a363f6849e03b00dd0a185ec4a59af"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c"
[[package]] [[package]]
name = "lru-slab" name = "lru-slab"
@@ -4032,9 +4069,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "lzma-rust2" name = "lzma-rust2"
version = "0.23.0" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa" checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee"
dependencies = [ dependencies = [
"sha2 0.11.1", "sha2 0.11.1",
] ]
@@ -4212,10 +4249,10 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"once_cell", "once_cell",
"png 0.18.1", "png",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4382,7 +4419,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4652,17 +4689,6 @@ dependencies = [
"objc2-core-foundation", "objc2-core-foundation",
] ]
[[package]]
name = "objc2-open-directory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
dependencies = [
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]] [[package]]
name = "objc2-osa-kit" name = "objc2-osa-kit"
version = "0.3.2" version = "0.3.2"
@@ -4764,7 +4790,7 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [ dependencies = [
"parking_lot_core", "parking_lot_core 0.9.12",
] ]
[[package]] [[package]]
@@ -4787,9 +4813,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "open" name = "open"
version = "5.4.4" version = "5.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
dependencies = [ dependencies = [
"dunce", "dunce",
"is-wsl", "is-wsl",
@@ -4859,7 +4885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -4923,8 +4949,18 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [ dependencies = [
"lock_api", "lock_api 0.4.14",
"parking_lot_core", "parking_lot_core 0.9.12",
]
[[package]]
name = "parking_lot"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9cf937ca4e1d0b7578784559063422fc39ae7c5a00f02471f2614d5486fd226"
dependencies = [
"lock_api 0.5.0",
"parking_lot_core 0.10.0",
] ]
[[package]] [[package]]
@@ -4935,7 +4971,19 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall", "redox_syscall 0.5.18",
"smallvec",
"windows-link 0.2.1",
]
[[package]]
name = "parking_lot_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cfff060068cc18aa7815010fcaf80c68c7fb9e85fc7f5c0f9bf142c21d19826"
dependencies = [
"libc",
"redox_syscall 0.9.4",
"smallvec", "smallvec",
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
@@ -5226,19 +5274,6 @@ dependencies = [
"plotters-backend", "plotters-backend",
] ]
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.8.9",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.18.1" version = "0.18.1"
@@ -5524,7 +5559,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -5668,6 +5703,15 @@ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
] ]
[[package]]
name = "redox_syscall"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
dependencies = [
"bitflags 2.13.2",
]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.4.6"
@@ -5865,9 +5909,9 @@ dependencies = [
[[package]] [[package]]
name = "rfd" name = "rfd"
version = "0.16.0" version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [ dependencies = [
"block2", "block2",
"dispatch2", "dispatch2",
@@ -5875,16 +5919,18 @@ dependencies = [
"gobject-sys 0.18.0", "gobject-sys 0.18.0",
"gtk-sys", "gtk-sys",
"js-sys", "js-sys",
"libc",
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation",
"percent-encoding",
"raw-window-handle", "raw-window-handle",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -6028,7 +6074,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -6041,7 +6087,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.12.1", "linux-raw-sys 0.12.1",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -6100,7 +6146,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -6478,7 +6524,7 @@ version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [ dependencies = [
"darling", "darling 0.23.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 2.0.119",
@@ -6720,7 +6766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -6738,7 +6784,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"redox_syscall", "redox_syscall 0.5.18",
"tracing", "tracing",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
@@ -6773,9 +6819,9 @@ dependencies = [
[[package]] [[package]]
name = "specta" name = "specta"
version = "2.0.0-rc.25" version = "2.0.0-rc.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50" checksum = "ed155e448f42fc4be7a5cbc12bd43f6c549981f7c32fcba429ff6bc57cc4f30c"
dependencies = [ dependencies = [
"paste", "paste",
"rustc_version 0.4.1", "rustc_version 0.4.1",
@@ -6785,9 +6831,9 @@ dependencies = [
[[package]] [[package]]
name = "specta-macros" name = "specta-macros"
version = "2.0.0-rc.25" version = "2.0.0-rc.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb" checksum = "b34b2516e16a9ddd3dc4e73cd6454cfe21fe209f8cb5a6f37346fe3774d759b3"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"proc-macro2", "proc-macro2",
@@ -6797,9 +6843,9 @@ dependencies = [
[[package]] [[package]]
name = "specta-serde" name = "specta-serde"
version = "0.0.12" version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029" checksum = "4983177ed0eb28235080261953de5f9ebae8a2d107851b5852420bd2ab246245"
dependencies = [ dependencies = [
"specta", "specta",
"specta-macros", "specta-macros",
@@ -6807,18 +6853,18 @@ dependencies = [
[[package]] [[package]]
name = "specta-typescript" name = "specta-typescript"
version = "0.0.12" version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0" checksum = "3ecbe75dc1d27b36141b50b02b7c73bed00945710cee6edffa86d296a159a6f9"
dependencies = [ dependencies = [
"specta", "specta",
] ]
[[package]] [[package]]
name = "specta-util" name = "specta-util"
version = "0.0.12" version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502" checksum = "d9bf531215ea70fba8264edecf98f12600462cf359f2038602a47aeda5ecd5b8"
dependencies = [ dependencies = [
"specta", "specta",
] ]
@@ -6848,7 +6894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [ dependencies = [
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot", "parking_lot 0.12.5",
"phf_shared 0.13.1", "phf_shared 0.13.1",
"precomputed-hash", "precomputed-hash",
] ]
@@ -6984,16 +7030,15 @@ dependencies = [
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
version = "0.39.6" version = "0.40.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
dependencies = [ dependencies = [
"libc", "libc",
"memchr", "memchr",
"ntapi", "ntapi",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-kit", "objc2-io-kit",
"objc2-open-directory",
"windows 0.62.2", "windows 0.62.2",
] ]
@@ -7095,7 +7140,7 @@ dependencies = [
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation",
"objc2-ui-kit", "objc2-ui-kit",
"parking_lot", "parking_lot 0.12.5",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"tao-macros", "tao-macros",
@@ -7149,9 +7194,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.12.4" version = "2.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212" checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -7204,9 +7249,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-build" name = "tauri-build"
version = "2.7.3" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc" checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo_toml", "cargo_toml",
@@ -7227,16 +7272,16 @@ dependencies = [
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.7.3" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f" checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.23.1",
"brotli", "brotli",
"ico", "ico",
"json-patch", "json-patch",
"plist", "plist",
"png 0.18.1", "png",
"proc-macro2", "proc-macro2",
"quote", "quote",
"semver 1.0.28", "semver 1.0.28",
@@ -7254,9 +7299,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-macros" name = "tauri-macros"
version = "2.7.3" version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f" checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -7302,7 +7347,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"deelevate", "deelevate",
"libc", "libc",
"parking_lot", "parking_lot 0.13.0",
"serde", "serde",
"specta", "specta",
"sysinfo", "sysinfo",
@@ -7327,9 +7372,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.6.2" version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4" checksum = "ab551d9bec27bb79d1c7fe5ed38c5cfd1cebb86efa745acc65f5853f345f6e56"
dependencies = [ dependencies = [
"dunce", "dunce",
"plist", "plist",
@@ -7348,9 +7393,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-devtools" name = "tauri-plugin-devtools"
version = "2.2.2" version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1acb4d1b8490ee79166b72c3f8c989e066650a11aa94dfd0d71fb05c61cdbed0" checksum = "299a62101a16c14b914ebfaf529b4b64ad9ca534a0ece1c21fc9308d1e3ba9db"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"bytes", "bytes",
@@ -7375,9 +7420,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-dialog" name = "tauri-plugin-dialog"
version = "2.8.3" version = "2.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b" checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5"
dependencies = [ dependencies = [
"log", "log",
"raw-window-handle", "raw-window-handle",
@@ -7481,13 +7526,13 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-notification" name = "tauri-plugin-notification"
version = "2.7.0" version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b" checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
dependencies = [ dependencies = [
"log", "log",
"notify-rust", "notify-rust",
"rand 0.9.5", "rand 0.10.2",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
@@ -7521,21 +7566,11 @@ dependencies = [
"zbus", "zbus",
] ]
[[package]]
name = "tauri-plugin-process"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74ae3d8549ae564c0397377373e262f340908eb23a5b9fbe03f2a00007dc0038"
dependencies = [
"tauri",
"tauri-plugin",
]
[[package]] [[package]]
name = "tauri-plugin-shell" name = "tauri-plugin-shell"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521" checksum = "7375b25f72f70f6168f27f1ba3a413e30eaa0a2098cff53a4046ebfe9e2f142d"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
"libc", "libc",
@@ -7590,9 +7625,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-plugin-wdio-webdriver" name = "tauri-plugin-wdio-webdriver"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e" checksum = "5a775b0b59f6f12b38d1f28667311a89cd07f532f540e27d65c6eadfba1156d8"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -7663,9 +7698,9 @@ dependencies = [
[[package]] [[package]]
name = "tauri-runtime-wry" name = "tauri-runtime-wry"
version = "2.12.2" version = "2.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae" checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f"
dependencies = [ dependencies = [
"gtk", "gtk",
"http", "http",
@@ -7673,7 +7708,6 @@ dependencies = [
"log", "log",
"objc2", "objc2",
"objc2-app-kit", "objc2-app-kit",
"once_cell",
"percent-encoding", "percent-encoding",
"raw-window-handle", "raw-window-handle",
"softbuffer", "softbuffer",
@@ -7690,11 +7724,12 @@ dependencies = [
[[package]] [[package]]
name = "tauri-specta" name = "tauri-specta"
version = "2.0.0-rc.25" version = "2.0.0-rc.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2" checksum = "fe71fc209fa4c49f959644f1ae2d70f72d5675bcb3d3ad8518122fa7b330994e"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"paste",
"serde", "serde",
"serde_json", "serde_json",
"specta", "specta",
@@ -7708,15 +7743,15 @@ dependencies = [
[[package]] [[package]]
name = "tauri-specta-macros" name = "tauri-specta-macros"
version = "2.0.0-rc.25" version = "2.0.0-rc.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012" checksum = "dbf5689a1dbf2611e2351b79ba042ca089d0a3ce45274695e79084cd732a5aec"
dependencies = [ dependencies = [
"darling", "darling 0.24.1",
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.119", "syn 3.0.5",
] ]
[[package]] [[package]]
@@ -7786,10 +7821,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.3.4", "getrandom 0.4.3",
"once_cell", "once_cell",
"rustix 1.1.4", "rustix 1.1.4",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -8042,9 +8077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.53.3" version = "1.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -8400,7 +8435,7 @@ dependencies = [
"flexi_logger", "flexi_logger",
"log", "log",
"nu-ansi-term", "nu-ansi-term",
"parking_lot", "parking_lot 0.12.5",
"tokio", "tokio",
"tracing", "tracing",
"tracing-log", "tracing-log",
@@ -8452,10 +8487,10 @@ dependencies = [
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation",
"once_cell", "once_cell",
"png 0.18.1", "png",
"serde", "serde",
"thiserror 2.0.20", "thiserror 2.0.20",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -8564,7 +8599,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -9010,9 +9045,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.9" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -9106,7 +9141,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -9127,7 +9162,7 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
"objc2-quartz-core", "objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.60.2", "windows-sys 0.61.2",
"windows-version", "windows-version",
] ]
@@ -9702,7 +9737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -10034,9 +10069,9 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "9.1.3" version = "9.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb" checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426"
dependencies = [ dependencies = [
"aes", "aes",
"bzip2", "bzip2",
@@ -10047,6 +10082,7 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"hmac", "hmac",
"indexmap 2.14.0", "indexmap 2.14.0",
"libc",
"lzma-rust2", "lzma-rust2",
"memchr", "memchr",
"nix", "nix",
+4 -6
View File
@@ -61,21 +61,20 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" } tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1.44" tracing = "0.1.44"
tauri = { version = "2.12.4" } tauri = { version = "2.12.5" }
tauri-plugin-clipboard-manager = "2.5.0" tauri-plugin-clipboard-manager = "2.5.0"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] } parking_lot = "0.13.0"
anyhow = "1.0.104" anyhow = "1.0.104"
chrono = "0.4.45" chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] } criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.53.3", features = [ tokio = { version = "1.54.1", features = [
"rt-multi-thread", "rt-multi-thread",
"macros", "macros",
"time", "time",
"sync", "sync",
] } ] }
flexi_logger = "0.31.10" flexi_logger = "0.31.10"
log = "0.4.34" log = "0.4.35"
regex = "1.13.1"
reqwest = { version = "0.13.5", features = [ reqwest = { version = "0.13.5", features = [
"json", "json",
"cookies", "cookies",
@@ -90,7 +89,6 @@ smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" } serde = { version = "1.0.229" }
serde_json = { version = "1.0.154" } serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" } serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
socket2 = "0.6.5" socket2 = "0.6.5"
# *** For Windows platform only *** # *** For Windows platform only ***
+11 -7
View File
@@ -19,19 +19,18 @@
scripts-workflow/group_platforms.sh regroups, --check verifies scripts-workflow/group_platforms.sh regroups, --check verifies
HTML comments are stripped by the update dialog's sanitizer and hidden on HTML comments are stripped by the update dialog's sanitizer and hidden on
GitHub, so they never reach users. --> GitHub, so they never reach users. -->
## v2.5.10 ## v2.6.0
<details> <details>
<summary><strong> 🐞 修复问题 </strong></summary> <summary><strong> 🐞 修复问题 </strong></summary>
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口 - 修复检测更新后又发布新版时,应用内更新失败
- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动 - 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验
- 修复订阅的 DNS 配置变化后,「DNS 覆写」实际已关闭但开关仍显示开启且没有提示
**🖥️ Windows** **🍎 macOS**
- Windows:修复安装程序重新安装或覆盖时未提示退出运行中应用,导致卸载报错或覆盖失败 - 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
</details> </details>
@@ -44,5 +43,10 @@
<details> <details>
<summary><strong> 🚀 优化改进 </strong></summary> <summary><strong> 🚀 优化改进 </strong></summary>
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
**🍎 macOS**
- 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址
</details> </details>
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] } specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
clash-verge-logging = { workspace = true } clash-verge-logging = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
log = { workspace = true } log = { workspace = true }
@@ -5,14 +5,14 @@ edition = "2024"
rust-version = "1.91" rust-version = "1.91"
[dependencies] [dependencies]
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] } specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
tauri = { workspace = true } tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true } tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true } parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light # sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623 # see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.39.6", features = ["network", "system"] } sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies] [target.'cfg(not(windows))'.dependencies]
libc = "0.2.191" libc = "0.2.191"
@@ -27,9 +27,9 @@ pub struct SysInfo {
impl Default for SysInfo { impl Default for SysInfo {
#[inline] #[inline]
fn default() -> Self { fn default() -> Self {
let system_name = System::name().unwrap_or_else(|| "Null".into()); let system_name = System::name().unwrap_or_else(|_| "Null".into());
let system_version = System::long_os_version().unwrap_or_else(|| "Null".into()); let system_version = System::long_os_version().unwrap_or_else(|_| "Null".into());
let system_kernel_version = System::kernel_version().unwrap_or_else(|| "Null".into()); let system_kernel_version = System::kernel_version().unwrap_or_else(|_| "Null".into());
let system_arch = System::cpu_arch(); let system_arch = System::cpu_arch();
Self { Self {
system_name, system_name,
@@ -130,9 +130,14 @@ pub fn current_gid() -> u32 {
#[inline] #[inline]
pub fn list_network_interfaces() -> Vec<String> { pub fn list_network_interfaces() -> Vec<String> {
let mut networks = Networks::new(); let Ok(mut networks) = Networks::new() else {
return Vec::new();
};
networks.refresh(false); networks.refresh(false);
networks.keys().map(|name| name.to_owned()).collect() networks
.keys()
.map(|name| name.to_string_lossy().into_owned())
.collect()
} }
#[inline] #[inline]
+29
View File
@@ -1,3 +1,32 @@
## v2.5.10
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口
- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动
- 修复服务模式下,内核更新的 GEO 数据库不会同步回应用目录,导致使用新分类的规则无法通过校验
- 修复订阅的 DNS 配置变化后,「DNS 覆写」实际已关闭但开关仍显示开启且没有提示
**🖥️ Windows**
- Windows:修复安装程序重新安装或覆盖时未提示退出运行中应用,导致卸载报错或覆盖失败
</details>
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
</details>
## v2.5.9 ## v2.5.9
- **Mihomo(Meta) 内核升级至 v1.19.33** - **Mihomo(Meta) 内核升级至 v1.19.33**
+4 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.5.10", "version": "2.6.0",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"prepare": "husky || true", "prepare": "husky || true",
@@ -56,7 +56,7 @@
"@tanstack/react-virtual": "^3.14.13", "@tanstack/react-virtual": "^3.14.13",
"@tauri-apps/api": "2.12.4", "@tauri-apps/api": "2.12.4",
"@tauri-apps/plugin-clipboard-manager": "2.5.0", "@tauri-apps/plugin-clipboard-manager": "2.5.0",
"@tauri-apps/plugin-dialog": "2.8.3", "@tauri-apps/plugin-dialog": "2.8.4",
"@tauri-apps/plugin-fs": "2.7.1", "@tauri-apps/plugin-fs": "2.7.1",
"@tauri-apps/plugin-http": "2.8.3", "@tauri-apps/plugin-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2", "@tauri-apps/plugin-opener": "2.7.2",
@@ -94,7 +94,7 @@
"@biomejs/biome": "^2.5.14", "@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5", "@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.3", "@tauri-apps/cli": "2.12.4",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^26.0.0", "@types/node": "^26.0.0",
"@types/react": "19.3.0", "@types/react": "19.3.0",
@@ -102,7 +102,7 @@
"@types/validator": "^13.15.10", "@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3", "@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1", "@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.5.0", "@wdio/tauri-service": "1.5.1",
"adm-zip": "^0.6.1", "adm-zip": "^0.6.1",
"axios": "^1.20.0", "axios": "^1.20.0",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
@@ -117,7 +117,6 @@
"eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-react-refresh": "^0.5.7",
"eslint-plugin-sukka": "^10.2.0", "eslint-plugin-sukka": "^10.2.0",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"glob": "^14.0.0",
"globals": "^17.12.0", "globals": "^17.12.0",
"https-proxy-agent": "^9.1.0", "https-proxy-agent": "^9.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
+190 -226
View File
@@ -206,8 +206,8 @@ importers:
specifier: 2.5.0 specifier: 2.5.0
version: 2.5.0 version: 2.5.0
'@tauri-apps/plugin-dialog': '@tauri-apps/plugin-dialog':
specifier: 2.8.3 specifier: 2.8.4
version: 2.8.3 version: 2.8.4
'@tauri-apps/plugin-fs': '@tauri-apps/plugin-fs':
specifier: 2.7.1 specifier: 2.7.1
version: 2.7.1 version: 2.7.1
@@ -315,8 +315,8 @@ importers:
specifier: ^10.0.1 specifier: ^10.0.1
version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0)) version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: 2.12.3 specifier: 2.12.4
version: 2.12.3 version: 2.12.4
'@types/lodash-es': '@types/lodash-es':
specifier: ^4.17.12 specifier: ^4.17.12
version: 4.17.12 version: 4.17.12
@@ -334,13 +334,13 @@ importers:
version: 13.15.10 version: 13.15.10
'@vitejs/plugin-legacy': '@vitejs/plugin-legacy':
specifier: ^8.2.3 specifier: ^8.2.3
version: 8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) version: 8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.1.1 specifier: ^6.1.1
version: 6.1.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) version: 6.1.1(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
'@wdio/tauri-service': '@wdio/tauri-service':
specifier: 1.5.0 specifier: 1.5.1
version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)) version: 1.5.1(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
adm-zip: adm-zip:
specifier: ^0.6.1 specifier: ^0.6.1
version: 0.6.1 version: 0.6.1
@@ -383,9 +383,6 @@ importers:
eslint-plugin-unused-imports: eslint-plugin-unused-imports:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0)) version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
glob:
specifier: ^14.0.0
version: 14.0.3
globals: globals:
specifier: ^17.12.0 specifier: ^17.12.0
version: 17.12.0 version: 17.12.0
@@ -424,13 +421,13 @@ importers:
version: 8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) version: 8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
vite: vite:
specifier: ^8.3.0 specifier: ^8.3.0
version: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) version: 8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
vite-plugin-svgr: vite-plugin-svgr:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) version: 5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
vitest: vitest:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.1(@types/node@26.6.5)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) version: 5.0.1(@types/node@26.6.5)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
webdriverio: webdriverio:
specifier: 10.1.0 specifier: 10.1.0
version: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0) version: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
@@ -1384,9 +1381,6 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@isaacs/nonrecursive-glob-match@1.0.2':
resolution: {integrity: sha512-GJPHIgPPtFl+nLhFHyF0zZoFcOFLsZWbHFFzo+Zr+PWDXJbl9hHxb2w+9SiF2fHmLgKaXIpUzQo3SbBBZ9WObw==}
'@jest/diff-sequences@30.5.0': '@jest/diff-sequences@30.5.0':
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==} resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -1720,12 +1714,12 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@oxc-project/types@0.150.0':
resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
'@oxc-project/types@0.155.0': '@oxc-project/types@0.155.0':
resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==} resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
'@oxc-project/types@0.156.0':
resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==}
'@oxc-resolver/binding-android-arm-eabi@11.24.2': '@oxc-resolver/binding-android-arm-eabi@11.24.2':
resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==}
cpu: [arm] cpu: [arm]
@@ -1936,98 +1930,98 @@ packages:
'@remix-run/route-pattern@0.22.1': '@remix-run/route-pattern@0.22.1':
resolution: {integrity: sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==} resolution: {integrity: sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==}
'@rolldown/binding-android-arm-eabi@1.2.9': '@rolldown/binding-android-arm-eabi@1.2.16':
resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==} resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
'@rolldown/binding-android-arm64@1.2.9': '@rolldown/binding-android-arm64@1.2.16':
resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==} resolution: {integrity: sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@rolldown/binding-darwin-arm64@1.2.9': '@rolldown/binding-darwin-arm64@1.2.16':
resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==} resolution: {integrity: sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@rolldown/binding-darwin-x64@1.2.9': '@rolldown/binding-darwin-x64@1.2.16':
resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==} resolution: {integrity: sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@rolldown/binding-freebsd-x64@1.2.9': '@rolldown/binding-freebsd-x64@1.2.16':
resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==} resolution: {integrity: sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.2.9': '@rolldown/binding-linux-arm-gnueabihf@1.2.16':
resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==} resolution: {integrity: sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.2.9': '@rolldown/binding-linux-arm64-gnu@1.2.16':
resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==} resolution: {integrity: sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.2.9': '@rolldown/binding-linux-arm64-musl@1.2.16':
resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==} resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.2.9': '@rolldown/binding-linux-ppc64-gnu@1.2.16':
resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==} resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.9': '@rolldown/binding-linux-s390x-gnu@1.2.16':
resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==} resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.9': '@rolldown/binding-linux-x64-gnu@1.2.16':
resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==} resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.2.9': '@rolldown/binding-linux-x64-musl@1.2.16':
resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==} resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@rolldown/binding-openharmony-arm64@1.2.9': '@rolldown/binding-openharmony-arm64@1.2.16':
resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==} resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [openharmony] os: [openharmony]
'@rolldown/binding-win32-arm64-msvc@1.2.9': '@rolldown/binding-win32-arm64-msvc@1.2.16':
resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==} resolution: {integrity: sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.9': '@rolldown/binding-win32-x64-msvc@1.2.16':
resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==} resolution: {integrity: sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -2127,87 +2121,87 @@ packages:
'@tauri-apps/api@2.12.4': '@tauri-apps/api@2.12.4':
resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==} resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==}
'@tauri-apps/cli-darwin-arm64@2.12.3': '@tauri-apps/cli-darwin-arm64@2.12.4':
resolution: {integrity: sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==} resolution: {integrity: sha512-OxqKSckIkoJ6vX2AjifIJpTasFC0Hbdoo7gEiCmAjauABO3U8m2MRsLWe+QTiBwDx1mIrvgA+rvI7tvDnbnHVg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@tauri-apps/cli-darwin-x64@2.12.3': '@tauri-apps/cli-darwin-x64@2.12.4':
resolution: {integrity: sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==} resolution: {integrity: sha512-fz76/77wMMJOaaUN6n3CybRG0P/MTEjjqYk81mGPyhi9m6Ue5T+LWBnJP/AkPooNatBPI2vtulGqomyMJ44lCQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.3': '@tauri-apps/cli-linux-arm-gnueabihf@2.12.4':
resolution: {integrity: sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==} resolution: {integrity: sha512-K0w8Bof6Lt8QKLAYhRy0yy36ZH5iA/35VuSOvnxyjvCUZ+A3JKsUTlALviH/sVgAlisaPzzmeJSH8XOn4Q2HBA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.12.3': '@tauri-apps/cli-linux-arm64-gnu@2.12.4':
resolution: {integrity: sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==} resolution: {integrity: sha512-j5rOnjmRUwWrm1oosf0zBYl4D+fqydgUROFCk0hvKZxvbTT8SOERes0QB7rBV/XWNy+kM6pCKzb3oSm2sRglzg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.12.3': '@tauri-apps/cli-linux-arm64-musl@2.12.4':
resolution: {integrity: sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==} resolution: {integrity: sha512-aOue3Fy6Jb7+oWcfAVUsVHfeTybSCt2CZoU06aO4jv4d2Ks/KnaTPwMAK3CLEduGOvBLY1lbBBrIyomqLLH5Sg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.12.3': '@tauri-apps/cli-linux-riscv64-gnu@2.12.4':
resolution: {integrity: sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==} resolution: {integrity: sha512-Fx6GShY/RvST+t6LMlTHAwd66+1tEaAAGrnU7cysVfaiRP1BKng5Vl6rVmO6rSLOA1Sp1dRzNYM4f0OWQGb7qA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.12.3': '@tauri-apps/cli-linux-x64-gnu@2.12.4':
resolution: {integrity: sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==} resolution: {integrity: sha512-tNzZSa79Z1nRpHxEpQ38lsAFpKlFlYvoXzjTV+lRRn/zSf3wrdUC3wnLcq2XEadchPu4ttK0zivhYzr+XGD19Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc] libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.12.3': '@tauri-apps/cli-linux-x64-musl@2.12.4':
resolution: {integrity: sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==} resolution: {integrity: sha512-jmZ1pr0xEqUhuEusWtt9Yt+GF6lCnGHUmIL+KIknpbjK/XQ2qkhDBhaxK/JMHM2A8dAacVXCxXJk2EXpwhQUfA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl] libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.12.3': '@tauri-apps/cli-win32-arm64-msvc@2.12.4':
resolution: {integrity: sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==} resolution: {integrity: sha512-fGo8pgEHZxdSYgZ/J98m1usJ7NHKmKbq7k6uT0x1rpOq6Bg2YVzzpaAblWk710f5LHYBJIL5QIFS5IrD+4gmTg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.12.3': '@tauri-apps/cli-win32-ia32-msvc@2.12.4':
resolution: {integrity: sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==} resolution: {integrity: sha512-ezl8sQIcjYsye3TUnTH5VcWmWzccZASlQm7hlZt0dYyj1XD0qpVW47u2xYVrIKui0F8uQv/7QGZixdWiClBsbg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.12.3': '@tauri-apps/cli-win32-x64-msvc@2.12.4':
resolution: {integrity: sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==} resolution: {integrity: sha512-DJhDeCqjLDNMWYFy7ZS8cfeAHX2RK0RbQMLiA3bMVVkXRDJ4NBYiHriGqsUhsr9iG41aTb+dJGkFPmPw1gWwVw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@tauri-apps/cli@2.12.3': '@tauri-apps/cli@2.12.4':
resolution: {integrity: sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==} resolution: {integrity: sha512-JI9bHfudSRJikGOrjbXEm6RjzzvmKuLc+JixB1NE8hNzQkNlCCIC3pTES3uypkR9lxHrsbxzcnSwhI2IxBQphw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.5.0': '@tauri-apps/plugin-clipboard-manager@2.5.0':
resolution: {integrity: sha512-dibUy1nKURYDXRMvjNk4deAbTZ5jPQ8wCE4nolt5xniryWgXQ4sK2GkFXoXlHbRdPzfZptZj95BRLfalA7m/2Q==} resolution: {integrity: sha512-dibUy1nKURYDXRMvjNk4deAbTZ5jPQ8wCE4nolt5xniryWgXQ4sK2GkFXoXlHbRdPzfZptZj95BRLfalA7m/2Q==}
'@tauri-apps/plugin-dialog@2.8.3': '@tauri-apps/plugin-dialog@2.8.4':
resolution: {integrity: sha512-Hd/yBfk2R5v2MFrz/0VArSXeh8xKLLXGunqSiTtcHKn1zNZwaKujuA0VSwUvNfwC4hDoF1PveS+gBDNe3nHspw==} resolution: {integrity: sha512-+6ZOiXenOaxXwb8qCbBPdT2jX2cFPaVVHGIskrpmJLpmWj+qKRTD6X/z9x7hTdSQn7NedcoXCkWGej4a7dC4lw==}
'@tauri-apps/plugin-fs@2.7.1': '@tauri-apps/plugin-fs@2.7.1':
resolution: {integrity: sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==} resolution: {integrity: sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==}
@@ -2623,20 +2617,20 @@ packages:
resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==} resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-core@1.3.0': '@wdio/native-core@1.3.1':
resolution: {integrity: sha512-wUwc0QTdvhD0LwH4YcRaN9JWLX/yac8sovddUR5/4sqHMNJr3yaN11bpIfYoD/bEk+1nTLjWuDCFpe5l5wUgLQ==} resolution: {integrity: sha512-fdGk2lEr2vS+bClJXuZEnsG30cLzQv+B9W+dAt/Yte/fE33s/vDBn7hHwFx8aZLTOS0pPExErUxYiZEMXX+04g==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-spy@1.4.0': '@wdio/native-spy@1.4.1':
resolution: {integrity: sha512-unsC2Vgu5T9atD478Zj/p2N+rVSK4i/NibIaaEw4rZF/T5u5bmAHChFntEiBEfyMzd62VwbhKhzwwOSKjzrfJA==} resolution: {integrity: sha512-KGOmgaicHqDkqj+s2bxXsRsb27IzW6carlyTu1xcrJI34yUbC0v14F8mbhuZcewW+mt2xhmC17yITwAbNa8PKw==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-types@2.6.0': '@wdio/native-types@2.6.1':
resolution: {integrity: sha512-MVMVpZTH+UY6nUtLClCzY4BQSIs/nCCHo81i4ClLR+b2NehyBvwccttXDAGPgf0icyHUhd+PgIPXaLgRcm4M9A==} resolution: {integrity: sha512-Rm5chX+MrRhVgJYcYdcX/yyTZaLjxvi0w/fnSgPaDT16gMIntXlk22K2x3hwEdaUonOKepZyhh4jr9ZgRz7UaQ==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/native-utils@2.8.0': '@wdio/native-utils@2.8.1':
resolution: {integrity: sha512-TmIkPAd19FkJ747AdFVEtawbakVV3HHCh8zk3Kl/FLc6B2bbea7hn90jtLxqBZgCVqUuJIBcMNFg/nV/hgBFOw==} resolution: {integrity: sha512-QbmZjGkLUC2bmUJSzyqsHM11HND1D7tkxHFssUV1Kmx43+oPAeHHVR0NSZu2hMzSNrDZMBAmNYycR+3X8rqWcA==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
peerDependencies: peerDependencies:
tsx: ^4.22.4 tsx: ^4.22.4
@@ -2660,8 +2654,8 @@ packages:
resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==} resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
'@wdio/tauri-service@1.5.0': '@wdio/tauri-service@1.5.1':
resolution: {integrity: sha512-S3fs8T+jgNOFi4x/X2z69unS/1K3ZB28+owBouhHtVnrMKeBWAKP46IHnp8c8AZXppn6fiPNC4M1jfRZbLKeKw==} resolution: {integrity: sha512-nfzJU2x7bxvIXtYo+yvrX4Xwuba8h5hXOlJ0IyHanGg0ELavY/2UfyxygJDTcSGO+pCQMdC9JJEyMvaS50AtZQ==}
engines: {node: '>=18.20.0'} engines: {node: '>=18.20.0'}
peerDependencies: peerDependencies:
webdriverio: ^9.0.0 webdriverio: ^9.0.0
@@ -3508,8 +3502,8 @@ packages:
resolution: {integrity: sha512-dUnV6Uok578HuGYTC/EM0LqeXewXBCHsh/kmLkdcqN1zgSy7K3gCIrshPBS5JwW20XSaRv4Vnz7z8lMjOV8jWw==} resolution: {integrity: sha512-dUnV6Uok578HuGYTC/EM0LqeXewXBCHsh/kmLkdcqN1zgSy7K3gCIrshPBS5JwW20XSaRv4Vnz7z8lMjOV8jWw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
follow-redirects@1.16.0: follow-redirects@1.16.2:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
peerDependencies: peerDependencies:
debug: '*' debug: '*'
@@ -3590,10 +3584,6 @@ packages:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
glob@14.0.3:
resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==}
engines: {node: 18 || 20 || >=22}
globals@17.12.0: globals@17.12.0:
resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4131,10 +4121,6 @@ packages:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
minimatch@11.0.2:
resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==}
engines: {node: 18 || 20 || >=22}
minipass@7.1.3: minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
@@ -4296,10 +4282,6 @@ packages:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
path-scurry@3.0.0:
resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==}
engines: {node: 18 || 20 || >=22}
path-type@4.0.0: path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -4527,8 +4509,8 @@ packages:
rgb2hex@0.2.5: rgb2hex@0.2.5:
resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==} resolution: {integrity: sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==}
rolldown@1.2.9: rolldown@1.2.16:
resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==} resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
@@ -4602,8 +4584,8 @@ packages:
snake-case@3.0.4: snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
source-map-js@1.2.1: source-map-js@1.2.2:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
source-map-support@0.5.21: source-map-support@0.5.21:
@@ -4825,16 +4807,16 @@ packages:
undici-types@8.9.0: undici-types@8.9.0:
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
undici@6.28.1: undici@6.29.1:
resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} resolution: {integrity: sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==}
engines: {node: '>=18.17'} engines: {node: '>=18.17'}
undici@7.29.1: undici@7.30.1:
resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==}
engines: {node: '>=20.18.1'} engines: {node: '>=20.18.1'}
undici@8.11.2: undici@8.12.3:
resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==}
engines: {node: '>=22.19.0'} engines: {node: '>=22.19.0'}
unicode-canonical-property-names-ecmascript@2.0.1: unicode-canonical-property-names-ecmascript@2.0.1:
@@ -4919,8 +4901,8 @@ packages:
peerDependencies: peerDependencies:
vite: '>=3.0.0' vite: '>=3.0.0'
vite@8.3.0: vite@8.3.3:
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} resolution: {integrity: sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -5156,12 +5138,12 @@ snapshots:
'@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8) '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8)
'@octokit/request': 10.0.16 '@octokit/request': 10.0.16
'@octokit/request-error': 7.1.2 '@octokit/request-error': 7.1.2
undici: 6.28.1 undici: 6.29.1
'@actions/http-client@3.0.2': '@actions/http-client@3.0.2':
dependencies: dependencies:
tunnel: 0.0.6 tunnel: 0.0.6
undici: 6.28.1 undici: 6.29.1
'@babel/code-frame@7.29.7': '@babel/code-frame@7.29.7':
dependencies: dependencies:
@@ -6260,8 +6242,6 @@ snapshots:
dependencies: dependencies:
minipass: 7.1.3 minipass: 7.1.3
'@isaacs/nonrecursive-glob-match@1.0.2': {}
'@jest/diff-sequences@30.5.0': {} '@jest/diff-sequences@30.5.0': {}
'@jest/expect-utils@30.5.2': '@jest/expect-utils@30.5.2':
@@ -6550,10 +6530,10 @@ snapshots:
'@oxc-parser/binding-win32-x64-msvc@0.155.0': '@oxc-parser/binding-win32-x64-msvc@0.155.0':
optional: true optional: true
'@oxc-project/types@0.150.0': {}
'@oxc-project/types@0.155.0': {} '@oxc-project/types@0.155.0': {}
'@oxc-project/types@0.156.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.24.2': '@oxc-resolver/binding-android-arm-eabi@11.24.2':
optional: true optional: true
@@ -6689,49 +6669,49 @@ snapshots:
'@remix-run/route-pattern@0.22.1': {} '@remix-run/route-pattern@0.22.1': {}
'@rolldown/binding-android-arm-eabi@1.2.9': '@rolldown/binding-android-arm-eabi@1.2.16':
optional: true optional: true
'@rolldown/binding-android-arm64@1.2.9': '@rolldown/binding-android-arm64@1.2.16':
optional: true optional: true
'@rolldown/binding-darwin-arm64@1.2.9': '@rolldown/binding-darwin-arm64@1.2.16':
optional: true optional: true
'@rolldown/binding-darwin-x64@1.2.9': '@rolldown/binding-darwin-x64@1.2.16':
optional: true optional: true
'@rolldown/binding-freebsd-x64@1.2.9': '@rolldown/binding-freebsd-x64@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.9': '@rolldown/binding-linux-arm-gnueabihf@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.9': '@rolldown/binding-linux-arm64-gnu@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-arm64-musl@1.2.9': '@rolldown/binding-linux-arm64-musl@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.9': '@rolldown/binding-linux-ppc64-gnu@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.9': '@rolldown/binding-linux-s390x-gnu@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-x64-gnu@1.2.9': '@rolldown/binding-linux-x64-gnu@1.2.16':
optional: true optional: true
'@rolldown/binding-linux-x64-musl@1.2.9': '@rolldown/binding-linux-x64-musl@1.2.16':
optional: true optional: true
'@rolldown/binding-openharmony-arm64@1.2.9': '@rolldown/binding-openharmony-arm64@1.2.16':
optional: true optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.9': '@rolldown/binding-win32-arm64-msvc@1.2.16':
optional: true optional: true
'@rolldown/binding-win32-x64-msvc@1.2.9': '@rolldown/binding-win32-x64-msvc@1.2.16':
optional: true optional: true
'@rolldown/pluginutils@1.0.1': {} '@rolldown/pluginutils@1.0.1': {}
@@ -6824,58 +6804,58 @@ snapshots:
'@tauri-apps/api@2.12.4': {} '@tauri-apps/api@2.12.4': {}
'@tauri-apps/cli-darwin-arm64@2.12.3': '@tauri-apps/cli-darwin-arm64@2.12.4':
optional: true optional: true
'@tauri-apps/cli-darwin-x64@2.12.3': '@tauri-apps/cli-darwin-x64@2.12.4':
optional: true optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.3': '@tauri-apps/cli-linux-arm-gnueabihf@2.12.4':
optional: true optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.12.3': '@tauri-apps/cli-linux-arm64-gnu@2.12.4':
optional: true optional: true
'@tauri-apps/cli-linux-arm64-musl@2.12.3': '@tauri-apps/cli-linux-arm64-musl@2.12.4':
optional: true optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.12.3': '@tauri-apps/cli-linux-riscv64-gnu@2.12.4':
optional: true optional: true
'@tauri-apps/cli-linux-x64-gnu@2.12.3': '@tauri-apps/cli-linux-x64-gnu@2.12.4':
optional: true optional: true
'@tauri-apps/cli-linux-x64-musl@2.12.3': '@tauri-apps/cli-linux-x64-musl@2.12.4':
optional: true optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.12.3': '@tauri-apps/cli-win32-arm64-msvc@2.12.4':
optional: true optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.12.3': '@tauri-apps/cli-win32-ia32-msvc@2.12.4':
optional: true optional: true
'@tauri-apps/cli-win32-x64-msvc@2.12.3': '@tauri-apps/cli-win32-x64-msvc@2.12.4':
optional: true optional: true
'@tauri-apps/cli@2.12.3': '@tauri-apps/cli@2.12.4':
optionalDependencies: optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.12.3 '@tauri-apps/cli-darwin-arm64': 2.12.4
'@tauri-apps/cli-darwin-x64': 2.12.3 '@tauri-apps/cli-darwin-x64': 2.12.4
'@tauri-apps/cli-linux-arm-gnueabihf': 2.12.3 '@tauri-apps/cli-linux-arm-gnueabihf': 2.12.4
'@tauri-apps/cli-linux-arm64-gnu': 2.12.3 '@tauri-apps/cli-linux-arm64-gnu': 2.12.4
'@tauri-apps/cli-linux-arm64-musl': 2.12.3 '@tauri-apps/cli-linux-arm64-musl': 2.12.4
'@tauri-apps/cli-linux-riscv64-gnu': 2.12.3 '@tauri-apps/cli-linux-riscv64-gnu': 2.12.4
'@tauri-apps/cli-linux-x64-gnu': 2.12.3 '@tauri-apps/cli-linux-x64-gnu': 2.12.4
'@tauri-apps/cli-linux-x64-musl': 2.12.3 '@tauri-apps/cli-linux-x64-musl': 2.12.4
'@tauri-apps/cli-win32-arm64-msvc': 2.12.3 '@tauri-apps/cli-win32-arm64-msvc': 2.12.4
'@tauri-apps/cli-win32-ia32-msvc': 2.12.3 '@tauri-apps/cli-win32-ia32-msvc': 2.12.4
'@tauri-apps/cli-win32-x64-msvc': 2.12.3 '@tauri-apps/cli-win32-x64-msvc': 2.12.4
'@tauri-apps/plugin-clipboard-manager@2.5.0': '@tauri-apps/plugin-clipboard-manager@2.5.0':
dependencies: dependencies:
'@tauri-apps/api': 2.12.4 '@tauri-apps/api': 2.12.4
'@tauri-apps/plugin-dialog@2.8.3': '@tauri-apps/plugin-dialog@2.8.4':
dependencies: dependencies:
'@tauri-apps/api': 2.12.4 '@tauri-apps/api': 2.12.4
@@ -7186,7 +7166,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.12.2': '@unrs/resolver-binding-win32-x64-msvc@1.12.2':
optional: true optional: true
'@vitejs/plugin-legacy@8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': '@vitejs/plugin-legacy@8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
dependencies: dependencies:
'@babel/core': 7.29.7(supports-color@7.2.0) '@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
@@ -7201,23 +7181,23 @@ snapshots:
regenerator-runtime: 0.14.1 regenerator-runtime: 0.14.1
systemjs: 6.15.1 systemjs: 6.15.1
terser: 5.51.2 terser: 5.51.2
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) vite: 8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': '@vitejs/plugin-react@6.1.1(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.1 '@rolldown/pluginutils': 1.0.1
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) vite: 8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
'@vitest/mocker@5.0.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': '@vitest/mocker@5.0.1(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))':
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.31 '@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.1 '@vitest/spy': 5.0.1
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 1.4.1 magic-string: 1.4.1
optionalDependencies: optionalDependencies:
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) vite: 8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
'@vitest/pretty-format@4.1.11': '@vitest/pretty-format@4.1.11':
dependencies: dependencies:
@@ -7275,11 +7255,11 @@ snapshots:
safe-regex2: 5.1.1 safe-regex2: 5.1.1
strip-ansi: 7.2.0 strip-ansi: 7.2.0
'@wdio/native-core@1.3.0(supports-color@7.2.0)': '@wdio/native-core@1.3.1(supports-color@7.2.0)':
dependencies: dependencies:
'@wdio/logger': 9.29.1 '@wdio/logger': 9.29.1
'@wdio/native-types': 2.6.0 '@wdio/native-types': 2.6.1
'@wdio/native-utils': 2.8.0(supports-color@7.2.0) '@wdio/native-utils': 2.8.1(supports-color@7.2.0)
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
get-port: 7.2.0 get-port: 7.2.0
tslib: 2.8.1 tslib: 2.8.1
@@ -7287,11 +7267,11 @@ snapshots:
- supports-color - supports-color
- tsx - tsx
'@wdio/native-spy@1.4.0': {} '@wdio/native-spy@1.4.1': {}
'@wdio/native-types@2.6.0': {} '@wdio/native-types@2.6.1': {}
'@wdio/native-utils@2.8.0(supports-color@7.2.0)': '@wdio/native-utils@2.8.1(supports-color@7.2.0)':
dependencies: dependencies:
'@wdio/logger': 9.29.1 '@wdio/logger': 9.29.1
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -7325,14 +7305,14 @@ snapshots:
easy-table: 1.2.0 easy-table: 1.2.0
pretty-ms: 9.3.1 pretty-ms: 9.3.1
'@wdio/tauri-service@1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))': '@wdio/tauri-service@1.5.1(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))':
dependencies: dependencies:
'@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)) '@wdio/globals': 9.31.3(expect-webdriverio@6.1.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0))
'@wdio/logger': 9.29.1 '@wdio/logger': 9.29.1
'@wdio/native-core': 1.3.0(supports-color@7.2.0) '@wdio/native-core': 1.3.1(supports-color@7.2.0)
'@wdio/native-spy': 1.4.0 '@wdio/native-spy': 1.4.1
'@wdio/native-types': 2.6.0 '@wdio/native-types': 2.6.1
'@wdio/native-utils': 2.8.0(supports-color@7.2.0) '@wdio/native-utils': 2.8.1(supports-color@7.2.0)
'@wdio/spec-reporter': 9.31.2 '@wdio/spec-reporter': 9.31.2
'@wdio/types': 9.31.2 '@wdio/types': 9.31.2
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -7458,7 +7438,7 @@ snapshots:
axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0):
dependencies: dependencies:
follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) follow-redirects: 1.16.2(debug@4.4.3(supports-color@7.2.0))
form-data: 4.0.6 form-data: 4.0.6
https-proxy-agent: 5.0.1(supports-color@7.2.0) https-proxy-agent: 5.0.1(supports-color@7.2.0)
proxy-from-env: 2.1.0 proxy-from-env: 2.1.0
@@ -7628,7 +7608,7 @@ snapshots:
parse5: 7.3.0 parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0 parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2 parse5-parser-stream: 7.1.2
undici: 7.29.1 undici: 7.30.1
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
chokidar@5.0.0: chokidar@5.0.0:
@@ -7850,7 +7830,7 @@ snapshots:
'@wdio/logger': 10.1.0 '@wdio/logger': 10.1.0
'@zip.js/zip.js': 2.24.0 '@zip.js/zip.js': 2.24.0
decamelize: 6.0.1 decamelize: 6.0.1
undici: 8.11.2 undici: 8.12.3
electron-to-chromium@1.5.444: {} electron-to-chromium@1.5.444: {}
@@ -8309,7 +8289,7 @@ snapshots:
fnv1a52@1.0.0: {} fnv1a52@1.0.0: {}
follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): follow-redirects@1.16.2(debug@4.4.3(supports-color@7.2.0)):
optionalDependencies: optionalDependencies:
debug: 4.4.3(supports-color@7.2.0) debug: 4.4.3(supports-color@7.2.0)
@@ -8351,7 +8331,7 @@ snapshots:
'@zip.js/zip.js': 2.24.0 '@zip.js/zip.js': 2.24.0
decamelize: 6.0.1 decamelize: 6.0.1
modern-tar: 0.8.5 modern-tar: 0.8.5
undici: 8.11.2 undici: 8.12.3
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
@@ -8393,12 +8373,6 @@ snapshots:
minipass: 7.1.3 minipass: 7.1.3
path-scurry: 2.0.2 path-scurry: 2.0.2
glob@14.0.3:
dependencies:
minimatch: 11.0.2
minipass: 7.1.3
path-scurry: 3.0.0
globals@17.12.0: {} globals@17.12.0: {}
gopd@1.2.0: {} gopd@1.2.0: {}
@@ -9089,11 +9063,6 @@ snapshots:
dependencies: dependencies:
brace-expansion: 5.0.12 brace-expansion: 5.0.12
minimatch@11.0.2:
dependencies:
'@isaacs/nonrecursive-glob-match': 1.0.2
brace-expansion: 5.0.12
minipass@7.1.3: {} minipass@7.1.3: {}
minizlib@3.1.0: minizlib@3.1.0:
@@ -9294,11 +9263,6 @@ snapshots:
lru-cache: 11.5.3 lru-cache: 11.5.3
minipass: 7.1.3 minipass: 7.1.3
path-scurry@3.0.0:
dependencies:
lru-cache: 11.5.3
minipass: 7.1.3
path-type@4.0.0: {} path-type@4.0.0: {}
pathe@2.0.3: {} pathe@2.0.3: {}
@@ -9314,7 +9278,7 @@ snapshots:
dependencies: dependencies:
nanoid: 3.3.19 nanoid: 3.3.19
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.2
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
@@ -9524,26 +9488,26 @@ snapshots:
rgb2hex@0.2.5: {} rgb2hex@0.2.5: {}
rolldown@1.2.9: rolldown@1.2.16:
dependencies: dependencies:
'@oxc-project/types': 0.150.0 '@oxc-project/types': 0.156.0
'@rolldown/pluginutils': 1.0.1 '@rolldown/pluginutils': 1.0.1
optionalDependencies: optionalDependencies:
'@rolldown/binding-android-arm-eabi': 1.2.9 '@rolldown/binding-android-arm-eabi': 1.2.16
'@rolldown/binding-android-arm64': 1.2.9 '@rolldown/binding-android-arm64': 1.2.16
'@rolldown/binding-darwin-arm64': 1.2.9 '@rolldown/binding-darwin-arm64': 1.2.16
'@rolldown/binding-darwin-x64': 1.2.9 '@rolldown/binding-darwin-x64': 1.2.16
'@rolldown/binding-freebsd-x64': 1.2.9 '@rolldown/binding-freebsd-x64': 1.2.16
'@rolldown/binding-linux-arm-gnueabihf': 1.2.9 '@rolldown/binding-linux-arm-gnueabihf': 1.2.16
'@rolldown/binding-linux-arm64-gnu': 1.2.9 '@rolldown/binding-linux-arm64-gnu': 1.2.16
'@rolldown/binding-linux-arm64-musl': 1.2.9 '@rolldown/binding-linux-arm64-musl': 1.2.16
'@rolldown/binding-linux-ppc64-gnu': 1.2.9 '@rolldown/binding-linux-ppc64-gnu': 1.2.16
'@rolldown/binding-linux-s390x-gnu': 1.2.9 '@rolldown/binding-linux-s390x-gnu': 1.2.16
'@rolldown/binding-linux-x64-gnu': 1.2.9 '@rolldown/binding-linux-x64-gnu': 1.2.16
'@rolldown/binding-linux-x64-musl': 1.2.9 '@rolldown/binding-linux-x64-musl': 1.2.16
'@rolldown/binding-openharmony-arm64': 1.2.9 '@rolldown/binding-openharmony-arm64': 1.2.16
'@rolldown/binding-win32-arm64-msvc': 1.2.9 '@rolldown/binding-win32-arm64-msvc': 1.2.16
'@rolldown/binding-win32-x64-msvc': 1.2.9 '@rolldown/binding-win32-x64-msvc': 1.2.16
safaridriver@3.0.0: {} safaridriver@3.0.0: {}
@@ -9561,7 +9525,7 @@ snapshots:
dependencies: dependencies:
chokidar: 5.0.0 chokidar: 5.0.0
immutable: 5.1.9 immutable: 5.1.9
source-map-js: 1.2.1 source-map-js: 1.2.2
optionalDependencies: optionalDependencies:
'@parcel/watcher': 2.6.0 '@parcel/watcher': 2.6.0
@@ -9599,7 +9563,7 @@ snapshots:
dot-case: 3.0.4 dot-case: 3.0.4
tslib: 2.8.1 tslib: 2.8.1
source-map-js@1.2.1: {} source-map-js@1.2.2: {}
source-map-support@0.5.21: source-map-support@0.5.21:
dependencies: dependencies:
@@ -9817,11 +9781,11 @@ snapshots:
undici-types@8.9.0: {} undici-types@8.9.0: {}
undici@6.28.1: {} undici@6.29.1: {}
undici@7.29.1: {} undici@7.30.1: {}
undici@8.11.2: {} undici@8.12.3: {}
unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-canonical-property-names-ecmascript@2.0.1: {}
@@ -9933,23 +9897,23 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
vite-plugin-svgr@5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)): vite-plugin-svgr@5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)):
dependencies: dependencies:
'@rollup/pluginutils': 5.4.0 '@rollup/pluginutils': 5.4.0
'@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3) '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0)
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) vite: 8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
- typescript - typescript
vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1): vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1):
dependencies: dependencies:
lightningcss: 1.33.0 lightningcss: 1.33.0
picomatch: 4.0.7 picomatch: 4.0.7
postcss: 8.5.28 postcss: 8.5.28
rolldown: 1.2.9 rolldown: 1.2.16
tinyglobby: 0.2.17 tinyglobby: 0.2.17
optionalDependencies: optionalDependencies:
'@types/node': 26.6.5 '@types/node': 26.6.5
@@ -9960,10 +9924,10 @@ snapshots:
terser: 5.51.2 terser: 5.51.2
yaml: 2.9.1 yaml: 2.9.1
vitest@5.0.1(@types/node@26.6.5)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)): vitest@5.0.1(@types/node@26.6.5)(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) '@vitest/mocker': 5.0.1(vite@8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))
chai: 6.2.2 chai: 6.2.2
es-module-lexer: 2.3.2 es-module-lexer: 2.3.2
expect-type: 1.4.0 expect-type: 1.4.0
@@ -9974,7 +9938,7 @@ snapshots:
tinybench: 6.1.4 tinybench: 6.1.4
tinyexec: 1.3.0 tinyexec: 1.3.0
tinyglobby: 0.2.17 tinyglobby: 0.2.17
vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) vite: 8.3.3(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 26.6.5 '@types/node': 26.6.5
@@ -10022,7 +9986,7 @@ snapshots:
'@wdio/utils': 10.1.0(supports-color@7.2.0)(yauzl@2.10.0) '@wdio/utils': 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
deepmerge-ts: 8.0.2 deepmerge-ts: 8.0.2
https-proxy-agent: 9.1.0(supports-color@7.2.0) https-proxy-agent: 9.1.0(supports-color@7.2.0)
undici: 8.11.2 undici: 8.12.3
ws: 8.22.1 ws: 8.22.1
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
+21
View File
@@ -113,6 +113,27 @@
"ghcr.io/devcontainers/features/rust" "ghcr.io/devcontainers/features/rust"
], ],
"groupName": "Rust toolchain" "groupName": "Rust toolchain"
},
{
"description": "Update the Specta contract toolchain together",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta", "tauri-specta", "specta-typescript"],
"groupName": "specta",
"groupSlug": "specta",
"prCreation": "immediate",
"automerge": false
},
{
"description": "Keep Specta aligned with the supported tauri-specta release",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta", "tauri-specta"],
"allowedVersions": "/^2\\.0\\.0-rc\\.26$/"
},
{
"description": "Keep the TypeScript exporter compatible with Specta rc.26",
"matchManagers": ["cargo"],
"matchPackageNames": ["specta-typescript"],
"allowedVersions": "/^0\\.0\\.13$/"
} }
], ],
"customManagers": [ "customManagers": [
+2
View File
@@ -345,6 +345,8 @@ function releaseBody(tag, sha, tags, publishing) {
.join('\n\n') || 'No new commits.' .join('\n\n') || 'No new commits.'
return `## ${titleFor(tag)} return `## ${titleFor(tag)}
@clash-verge-rev/release
Source: \`dev\` · [${sha}](${url}/commit/${sha}) \n${comparison} Source: \`dev\` · [${sha}](${url}/commit/${sha}) \n${comparison}
${ ${
publishing publishing
+7 -9
View File
@@ -6,7 +6,6 @@ import path from 'node:path'
import zlib from 'node:zlib' import zlib from 'node:zlib'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import { glob } from 'glob'
import { HttpsProxyAgent } from 'https-proxy-agent' import { HttpsProxyAgent } from 'https-proxy-agent'
import { extract } from 'tar' import { extract } from 'tar'
@@ -266,13 +265,13 @@ async function getLatestReleaseVersion() {
} }
if (!META_MAP[`${platform}-${arch}`]) { if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`) throw new Error(`mihomo unsupported platform "${platform}-${arch}"`)
} }
if (!META_ALPHA_MAP[`${platform}-${arch}`]) { if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`) throw new Error(`mihomo alpha unsupported platform "${platform}-${arch}"`)
} }
function clashMetaAlpha() { function mihomoAlpha() {
const name = META_ALPHA_MAP[`${platform}-${arch}`] const name = META_ALPHA_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32' const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz' const urlExt = isWin ? 'zip' : 'gz'
@@ -285,7 +284,7 @@ function clashMetaAlpha() {
} }
} }
function clashMeta() { function mihomo() {
const name = META_MAP[`${platform}-${arch}`] const name = META_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32' const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz' const urlExt = isWin ? 'zip' : 'gz'
@@ -519,7 +518,7 @@ const resolveServicePermission = async () => {
let hasChanges = false let hasChanges = false
for (const f of serviceExecutables) { for (const f of serviceExecutables) {
const files = glob.sync(path.join(SERVICE_DIR, f)) const files = fs.globSync(path.join(SERVICE_DIR, f))
for (const filePath of files) { for (const filePath of files) {
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath) const currentHash = await calculateFileHash(filePath)
@@ -733,13 +732,12 @@ const tasks = [
{ {
name: 'verge-mihomo-alpha', name: 'verge-mihomo-alpha',
func: () => func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())), getLatestAlphaVersion().then(() => resolveSidecar(mihomoAlpha())),
retry: 5, retry: 5,
}, },
{ {
name: 'verge-mihomo', name: 'verge-mihomo',
func: () => func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())),
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5, retry: 5,
}, },
// After both sidecar tasks: it hashes what they downloaded. // After both sidecar tasks: it hashes what they downloaded.
+50 -17
View File
@@ -32,7 +32,9 @@ function is_valid_ip() {
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent. # Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
function sc_value() { function sc_value() {
printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p" local dictionary
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
sed -n "s/^ *$2 : //p" <<<"$dictionary"
} }
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer. # Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
@@ -50,31 +52,62 @@ function dns_of() {
echo "$dns" echo "$dns"
} }
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1 [ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1 dns_servers=()
while read -r server; do
! is_valid_ip "$server" && echo "$server is not a valid IP address." >&2 && exit 1
dns_servers+=("$server")
done <<<"$1"
# Backups are keyed by service ID, which survives renaming the service and renumbering its interface. # Backups are keyed by service ID, which survives renaming the service and renumbering its interface.
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) || exit 1
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName) hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName) || exit 1
[ -z "$hardware_port" ] && echo "No primary network service." && exit 1 [ -z "$hardware_port" ] && echo "No primary network service; waiting for the network." >&2 && exit 75
original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; } original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; }
backup=".original_dns/$service_id" backup=".original_dns/$service_id"
mkdir -p .original_dns || exit 1 mkdir -p .original_dns || exit 1
# Older releases kept a single backup for whichever service was primary. # Key old single-slot backups before import so a later network switch cannot reassign them.
if [ -f .original_dns.txt ]; then for state_dir in "$PWD" "${2:-$PWD}"; do
if [ -f "$backup" ]; then if [ -f "$state_dir/.original_dns.txt" ]; then
rm -f .original_dns.txt || exit 1 mkdir -p "$state_dir/.original_dns" || exit 1
else if [ -f "$state_dir/.original_dns/$service_id" ]; then
mv .original_dns.txt "$backup" || exit 1 rm -f "$state_dir/.original_dns.txt" || exit 1
else
mv "$state_dir/.original_dns.txt" "$state_dir/.original_dns/$service_id" || exit 1
fi
fi fi
done
if [ ! -f "$backup" ] && [ -n "$2" ] && [ -f "$2/$backup" ]; then
cp "$2/$backup" ".original_dns/.$service_id" || exit 1
mv ".original_dns/.$service_id" "$backup" || exit 1
fi
backup_header='# clash-verge-dns-managed'
function save_backup() {
printf '%s\n%s\n' "$backup_header" "$1" >".original_dns/.$service_id" &&
mv ".original_dns/.$service_id" "$backup"
}
saved_dns=
legacy_backup=false
if [ -f "$backup" ]; then
saved_dns=$(cat "$backup") || exit 1
case "$saved_dns" in
"$backup_header"$'\n'*) saved_dns=${saved_dns#*$'\n'} ;;
*) legacy_backup=true ;;
esac
fi fi
# The placeholder is ours, not the user's: keep the backup taken before it was applied.
if [ "$original_dns" != "$1" ]; then if [ "$original_dns" != "$1" ]; then
echo "$original_dns" >"$backup" || exit 1 # Only an unmarked historical backup owns the old single-address placeholder.
networksetup -setdnsservers "$hardware_port" "$1" if [ "$original_dns" != "114.114.114.114" ] || [ "$legacy_backup" != true ]; then
save_backup "$original_dns" || exit 1
legacy_backup=false
fi
networksetup -setdnsservers "$hardware_port" "${dns_servers[@]}"
[ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; } [ "$(dns_of "$hardware_port")" = "$1" ] || { echo "Failed to set DNS for $hardware_port." >&2; exit 1; }
elif [ ! -f "$backup" ]; then fi
echo "empty" >"$backup" || exit 1 # Keep legacy ownership until the new list is verified, including a retry after a failed marker write.
if [ "$legacy_backup" = true ]; then
save_backup "$saved_dns" || exit 1
fi fi
+64 -35
View File
@@ -1,9 +1,11 @@
#!/bin/bash #!/bin/bash
[ $# -lt 1 ] && echo "Usage: $0 <placeholder IP address>" && exit 1 [ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && exit 1
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent. # Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
function sc_value() { function sc_value() {
printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p" local dictionary
dictionary=$(printf 'show %s\nquit\n' "$1" | scutil) || return 1
sed -n "s/^ *$2 : //p" <<<"$dictionary"
} }
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer. # Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
@@ -21,42 +23,69 @@ function dns_of() {
echo "$dns" echo "$dns"
} }
result=0 backup_header='# clash-verge-dns-managed'
# Older releases kept a single backup for whichever service was primary. completed_backups=()
if [ -f .original_dns.txt ]; then
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
if [ -z "$service_id" ] || ! mkdir -p .original_dns; then
result=1
elif [ -f ".original_dns/$service_id" ]; then
rm -f .original_dns.txt || result=1
elif ! mv .original_dns.txt ".original_dns/$service_id"; then
result=1
fi
fi
for backup in .original_dns/*; do function restore_dns() {
[ -f "$backup" ] || continue local result=0
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName) # Older releases kept a single backup for whichever service was primary.
# Kept for a service that is gone: there is nothing to write it back to. if [ -f .original_dns.txt ]; then
[ -n "$hardware_port" ] || continue service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
if ! current_dns=$(dns_of "$hardware_port"); then if [ -z "$service_id" ] || ! mkdir -p .original_dns; then
echo "Cannot read DNS servers of $hardware_port." >&2 result=1
result=1 elif [ -f ".original_dns/$service_id" ]; then
continue rm -f .original_dns.txt || result=1
elif ! mv .original_dns.txt ".original_dns/$service_id"; then
result=1
fi
fi fi
# A service no longer on the placeholder was changed by someone else, and that change stands.
if [ "$current_dns" = "$1" ]; then for backup in .original_dns/*; do
original_dns=$(cat "$backup") || { result=1; continue; } [ -f "$backup" ] || continue
# Older releases could back up the placeholder itself. # Current backups take precedence even after their DNS has already been restored.
[ "$original_dns" = "$1" ] && original_dns=empty if [ "$PWD" != "$state_dir" ] && [ -f "$state_dir/$backup" ]; then
networksetup -setdnsservers "$hardware_port" $original_dns completed_backups+=("$PWD/$backup")
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then continue
echo "Failed to restore DNS for $hardware_port." >&2 fi
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName) || { result=1; continue; }
# Kept for a service that is gone: there is nothing to write it back to.
[ -n "$hardware_port" ] || continue
if ! current_dns=$(dns_of "$hardware_port"); then
echo "Cannot read DNS servers of $hardware_port." >&2
result=1 result=1
continue continue
fi fi
fi original_dns=$(cat "$backup") || { result=1; continue; }
rm -f "$backup" || result=1 legacy_backup=false
done case "$original_dns" in
rmdir .original_dns 2>/dev/null "$backup_header"$'\n'*) original_dns=${original_dns#*$'\n'} ;;
*) legacy_backup=true ;;
esac
# A service no longer on its recorded placeholder was changed by someone else.
if [ "$current_dns" = "$1" ] || { [ "$legacy_backup" = true ] && [ "$current_dns" = "114.114.114.114" ]; }; then
networksetup -setdnsservers "$hardware_port" $original_dns
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
echo "Failed to restore DNS for $hardware_port." >&2
result=1
continue
fi
fi
completed_backups+=("$PWD/$backup")
done
return $result
}
result=0
state_dir=$PWD
restore_dns "$1" || result=1
if [ -n "$2" ] && [ "$2" != "$PWD" ]; then
{ cd "$2" && restore_dns "$1"; } || result=1
fi
# Remove superseded legacy backups first; a cleanup failure must leave current authority intact.
if [ "$result" -eq 0 ]; then
for ((index=${#completed_backups[@]}-1; index>=0; index--)); do
rm -f "${completed_backups[index]}" || { result=1; break; }
done
fi
rmdir "$state_dir/.original_dns" "${2:-$state_dir}/.original_dns" 2>/dev/null
exit $result exit $result
+16 -22
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "2.5.10" version = "2.6.0"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"] authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev" identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.7.3", features = [] } tauri-build = { version = "2.7.4", features = [] }
[dependencies] [dependencies]
clash-verge-draft = { workspace = true } clash-verge-draft = { workspace = true }
@@ -55,11 +55,10 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_yaml_ng = { workspace = true } serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] } smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true } socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] } warp = { version = "0.4.3", features = ["server"] }
open = "5.4.4" open = "5.4.5"
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] } rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0" dirs = "7.0.0"
dunce = "1.0.5" dunce = "1.0.5"
nanoid = "0.5" nanoid = "0.5"
@@ -67,50 +66,46 @@ chrono = { workspace = true }
boa_engine = "0.22.0" boa_engine = "0.22.0"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
reqwest = { workspace = true } reqwest = { workspace = true }
regex = { workspace = true }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [
"guard", "guard",
] } ] }
network-interface = { version = "2.0.5", features = ["serde"] } network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.5.0" tauri-plugin-shell = "2.6.0"
tauri-plugin-opener = "2.7.2" tauri-plugin-opener = "2.7.2"
tauri-plugin-dialog = "2.8.3" tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1" tauri-plugin-fs = "2.7.1"
tauri-plugin-process = "2.4.0" tauri-plugin-deep-link = "2.6.3"
tauri-plugin-deep-link = "2.6.2"
tauri-plugin-window-state = "2.5.0" tauri-plugin-window-state = "2.5.0"
zip = "9.1.3" zip = "9.2.2"
flate2 = "1.1.10" flate2 = "1.1.10"
reqwest_dav = "0.3.3" reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" } aes-gcm = { version = "0.11.1" }
base64 = "0.23.1" base64 = "0.23.1"
minisign-verify = "0.3.0"
sha2 = "0.11.1" sha2 = "0.11.1"
getrandom = "0.4.3" getrandom = "0.4.3"
futures = "0.3.34" futures = "0.3.34"
gethostname = "1.1.0" gethostname = "1.1.0"
scopeguard = "1.2.0" scopeguard = "1.2.0"
tauri-plugin-notification = "2.7.0" tauri-plugin-notification = "2.8.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] } backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3" tauri-plugin-http = "2.8.3"
tracing = { workspace = true } tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.2", optional = true } tauri-plugin-devtools = { version = "2.2.3", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", rev = "7a03d16f4d39b1bb885b7babe66b9e153e34b615" }
async-trait = "0.1.92" async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [ clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
"client", "client",
] } ] }
arc-swap = "1.9.2" arc-swap = "1.9.3"
tokio-rustls = "0.26.6" tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] } rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.9" webpki-roots = "1.0.11"
dark-light = "3.0.0" dark-light = "3.0.0"
bytes = "1.12.1" bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] } tokio-util = { version = "0.7.20", features = ["time"] }
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] } specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] } tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] }
specta-typescript = "=0.0.12" specta-typescript = "=0.0.13"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
libc = "0.2.191" libc = "0.2.191"
@@ -120,7 +115,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true } clash-verge-signal = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.5.0", optional = true } tauri-plugin-wdio-webdriver = { version = "=1.5.1", optional = true }
objc2 = "0.6.5" objc2 = "0.6.5"
objc2-foundation = { version = "0.3.2", features = [ objc2-foundation = { version = "0.3.2", features = [
"NSString", "NSString",
@@ -157,7 +152,6 @@ tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.14.1" tauri-plugin-updater = "2.14.1"
[dev-dependencies] [dev-dependencies]
criterion = { workspace = true }
tokio = { workspace = true, features = ["test-util"] } tokio = { workspace = true, features = ["test-util"] }
warp = { version = "0.4.3", features = ["test"] } warp = { version = "0.4.3", features = ["test"] }
-1
View File
@@ -73,7 +73,6 @@
"global-shortcut:allow-register-all", "global-shortcut:allow-register-all",
"global-shortcut:allow-unregister", "global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all", "global-shortcut:allow-unregister-all",
"process:allow-exit",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"dialog:default" "dialog:default"
+5 -3
View File
@@ -1,7 +1,10 @@
use super::CmdResult; use super::CmdResult;
use crate::{ use crate::{
cmd::StringifyErr as _, cmd::StringifyErr as _,
core::{SilentUpdater, updater::DownloadEvent}, core::{
SilentUpdater,
updater::{DownloadEvent, InstallUpdateOutcome},
},
feat, feat,
utils::dirs, utils::dirs,
}; };
@@ -61,8 +64,7 @@ pub async fn install_update(
app_handle: AppHandle, app_handle: AppHandle,
version: std::string::String, version: std::string::String,
on_event: Channel<DownloadEvent>, on_event: Channel<DownloadEvent>,
) -> CmdResult<bool> { ) -> CmdResult<InstallUpdateOutcome> {
let version: String = version.into();
SilentUpdater::global() SilentUpdater::global()
.install_update(&app_handle, &version, |event| { .install_update(&app_handle, &version, |event| {
let _ = on_event.send(event); let _ = on_event.send(event);
+13 -56
View File
@@ -184,84 +184,41 @@ impl IClashTemp {
_ => None, _ => None,
}) })
} }
#[cfg(not(target_os = "windows"))] fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 {
fn guard_redir_port(config: &Mapping) -> u16 {
let mut port = config let mut port = config
.get("redir-port") .get(key)
.and_then(|value| match value { .and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(), Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None, _ => None,
}) })
.unwrap_or(7895); .unwrap_or(default);
if port == 0 { if port == 0 {
port = 7895; port = default;
} }
port port
} }
#[cfg(not(target_os = "windows"))]
fn guard_redir_port(config: &Mapping) -> u16 {
Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR)
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn guard_tproxy_port(config: &Mapping) -> u16 { fn guard_tproxy_port(config: &Mapping) -> u16 {
let mut port = config Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY)
.get("tproxy-port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(network::ports::DEFAULT_TPROXY);
if port == 0 {
port = network::ports::DEFAULT_TPROXY;
}
port
} }
fn guard_mixed_port(config: &Mapping) -> u16 { fn guard_mixed_port(config: &Mapping) -> u16 {
let raw_value = config.get("mixed-port"); Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED)
let mut port = raw_value
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(network::ports::DEFAULT_MIXED);
if port == 0 {
port = network::ports::DEFAULT_MIXED;
}
port
} }
fn guard_socks_port(config: &Mapping) -> u16 { fn guard_socks_port(config: &Mapping) -> u16 {
let mut port = config Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS)
.get("socks-port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7898);
if port == 0 {
port = 7898;
}
port
} }
fn guard_port(config: &Mapping) -> u16 { fn guard_port(config: &Mapping) -> u16 {
let mut port = config Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP)
.get("port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7899);
if port == 0 {
port = 7899;
}
port
} }
pub(super) fn guard_server_ctrl(config: &Mapping) -> String { pub(super) fn guard_server_ctrl(config: &Mapping) -> String {
+9
View File
@@ -14,6 +14,15 @@ mod verge;
pub(crate) use self::config::Config; pub(crate) use self::config::Config;
pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*}; pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*};
macro_rules! patch_opt_field {
($lv:expr, $rv:expr, $key:tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key.clone();
}
};
}
pub(crate) use patch_opt_field;
pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) { pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
} }
+71 -108
View File
@@ -1,3 +1,5 @@
use super::patch_opt_field;
use crate::{ use crate::{
config::profiles, config::profiles,
utils::{ utils::{
@@ -131,19 +133,18 @@ impl PrfOption {
match (one, other) { match (one, other) {
(Some(a_ref), Some(b_ref)) => { (Some(a_ref), Some(b_ref)) => {
let mut result = a_ref.clone(); let mut result = a_ref.clone();
result.user_agent = b_ref.user_agent.clone().or(result.user_agent); patch_opt_field!(result, b_ref, user_agent);
result.with_proxy = b_ref.with_proxy.or(result.with_proxy); patch_opt_field!(result, b_ref, with_proxy);
result.self_proxy = b_ref.self_proxy.or(result.self_proxy); patch_opt_field!(result, b_ref, self_proxy);
result.danger_accept_invalid_certs = patch_opt_field!(result, b_ref, danger_accept_invalid_certs);
b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs); patch_opt_field!(result, b_ref, allow_auto_update);
result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); patch_opt_field!(result, b_ref, update_interval);
result.update_interval = b_ref.update_interval.or(result.update_interval); patch_opt_field!(result, b_ref, merge);
result.merge = b_ref.merge.clone().or(result.merge); patch_opt_field!(result, b_ref, script);
result.script = b_ref.script.clone().or(result.script); patch_opt_field!(result, b_ref, rules);
result.rules = b_ref.rules.clone().or(result.rules); patch_opt_field!(result, b_ref, proxies);
result.proxies = b_ref.proxies.clone().or(result.proxies); patch_opt_field!(result, b_ref, groups);
result.groups = b_ref.groups.clone().or(result.groups); patch_opt_field!(result, b_ref, timeout_seconds);
result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds);
Some(result) Some(result)
} }
(Some(a_ref), None) => Some(a_ref.clone()), (Some(a_ref), None) => Some(a_ref.clone()),
@@ -181,6 +182,38 @@ impl PrfItem {
} }
} }
async fn append_companion_item(item: &mut Self) -> Result<String> {
profiles::profiles_append_item_safe(item).await?;
item.uid
.clone()
.ok_or_else(|| anyhow::anyhow!("appended companion item has no uid"))
}
async fn ensure_companion_items(
merge: &mut Option<String>,
script: &mut Option<String>,
rules: &mut Option<String>,
proxies: &mut Option<String>,
groups: &mut Option<String>,
) -> Result<()> {
if merge.is_none() {
*merge = Some(Self::append_companion_item(&mut Self::from_merge(None)).await?);
}
if script.is_none() {
*script = Some(Self::append_companion_item(&mut Self::from_script(None)).await?);
}
if rules.is_none() {
*rules = Some(Self::append_companion_item(&mut Self::from_rules()).await?);
}
if proxies.is_none() {
*proxies = Some(Self::append_companion_item(&mut Self::from_proxies()).await?);
}
if groups.is_none() {
*groups = Some(Self::append_companion_item(&mut Self::from_groups()).await?);
}
Ok(())
}
async fn from_local( async fn from_local(
name: String, name: String,
desc: String, desc: String,
@@ -197,31 +230,7 @@ impl PrfItem {
let mut proxies = opt_ref.and_then(|o| o.proxies.clone()); let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
let mut groups = opt_ref.and_then(|o| o.groups.clone()); let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() { Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
let merge_item = &mut Self::from_merge(None);
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
}
if script.is_none() {
let script_item = &mut Self::from_script(None);
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
}
if rules.is_none() {
let rules_item = &mut Self::from_rules();
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
}
if proxies.is_none() {
let proxies_item = &mut Self::from_proxies();
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
}
if groups.is_none() {
let groups_item = &mut Self::from_groups();
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
Ok(Self { Ok(Self {
uid: Some(uid), uid: Some(uid),
itype: Some("local".into()), itype: Some("local".into()),
@@ -375,31 +384,7 @@ impl PrfItem {
bail!("profile does not contain `proxies` or `proxy-providers`"); bail!("profile does not contain `proxies` or `proxy-providers`");
} }
if merge.is_none() { Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?;
let merge_item = &mut Self::from_merge(None);
profiles::profiles_append_item_safe(merge_item).await?;
merge = merge_item.uid.clone();
}
if script.is_none() {
let script_item = &mut Self::from_script(None);
profiles::profiles_append_item_safe(script_item).await?;
script = script_item.uid.clone();
}
if rules.is_none() {
let rules_item = &mut Self::from_rules();
profiles::profiles_append_item_safe(rules_item).await?;
rules = rules_item.uid.clone();
}
if proxies.is_none() {
let proxies_item = &mut Self::from_proxies();
profiles::profiles_append_item_safe(proxies_item).await?;
proxies = proxies_item.uid.clone();
}
if groups.is_none() {
let groups_item = &mut Self::from_groups();
profiles::profiles_append_item_safe(groups_item).await?;
groups = groups_item.uid.clone();
}
Ok(Self { Ok(Self {
uid: Some(uid), uid: Some(uid),
@@ -461,46 +446,30 @@ impl PrfItem {
} }
} }
fn from_rules() -> Self { fn new_seq_item(prefix: &str, itype: &str, template: &str) -> Self {
let uid = help::get_uid("r").into(); let uid = help::get_uid(prefix).into();
let file = format!("{uid}.yaml").into(); // yaml ext let file = format!("{uid}.yaml").into(); // yaml ext
Self { Self {
uid: Some(uid), uid: Some(uid),
itype: Some("rules".into()), itype: Some(itype.into()),
file: Some(file), file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_RULES.into()), file_data: Some(template.into()),
..Default::default() ..Default::default()
} }
} }
fn from_rules() -> Self {
Self::new_seq_item("r", "rules", tmpl::ITEM_RULES)
}
fn from_proxies() -> Self { fn from_proxies() -> Self {
let uid = help::get_uid("p").into(); Self::new_seq_item("p", "proxies", tmpl::ITEM_PROXIES)
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
uid: Some(uid),
itype: Some("proxies".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_PROXIES.into()),
..Default::default()
}
} }
fn from_groups() -> Self { fn from_groups() -> Self {
let uid = help::get_uid("g").into(); Self::new_seq_item("g", "groups", tmpl::ITEM_GROUPS)
let file = format!("{uid}.yaml").into(); // yaml ext
Self {
uid: Some(uid),
itype: Some("groups".into()),
file: Some(file),
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_GROUPS.into()),
..Default::default()
}
} }
pub(crate) async fn read_file(&self) -> Result<String> { pub(crate) async fn read_file(&self) -> Result<String> {
@@ -516,26 +485,20 @@ impl PrfItem {
} }
} }
macro_rules! current_field {
($name:ident, $field:ident) => {
pub(crate) fn $name(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.$field.as_ref())
}
};
}
impl PrfItem { impl PrfItem {
pub(crate) fn current_merge(&self) -> Option<&String> { current_field!(current_merge, merge);
self.option.as_ref().and_then(|o| o.merge.as_ref()) current_field!(current_script, script);
} current_field!(current_rules, rules);
current_field!(current_proxies, proxies);
pub(crate) fn current_script(&self) -> Option<&String> { current_field!(current_groups, groups);
self.option.as_ref().and_then(|o| o.script.as_ref())
}
pub(crate) fn current_rules(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.rules.as_ref())
}
pub(crate) fn current_proxies(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.proxies.as_ref())
}
pub(crate) fn current_groups(&self) -> Option<&String> {
self.option.as_ref().and_then(|o| o.groups.as_ref())
}
} }
fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool { fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool {
+93 -62
View File
@@ -1,5 +1,5 @@
use super::{ use super::{
PrfOption, PrfOption, patch_opt_field,
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url}, prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
}; };
use crate::core::notify::{Refresh, announce}; use crate::core::notify::{Refresh, announce};
@@ -86,14 +86,6 @@ impl ProfileDeletePlan {
} }
} }
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key.to_owned();
}
};
}
impl IProfiles { impl IProfiles {
fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> { fn take_item_file_by_uid(items: &mut Vec<PrfItem>, target_uid: Option<&str>) -> Option<String> {
let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?; let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?;
@@ -244,15 +236,15 @@ impl IProfiles {
for each in items.iter_mut() { for each in items.iter_mut() {
if each.uid.as_deref() == Some(uid) { if each.uid.as_deref() == Some(uid) {
patch!(each, item, itype); patch_opt_field!(each, item, itype);
patch!(each, item, name); patch_opt_field!(each, item, name);
patch!(each, item, desc); patch_opt_field!(each, item, desc);
patch!(each, item, file); patch_opt_field!(each, item, file);
patch!(each, item, url); patch_opt_field!(each, item, url);
patch!(each, item, selected); patch_opt_field!(each, item, selected);
patch!(each, item, extra); patch_opt_field!(each, item, extra);
patch!(each, item, updated); patch_opt_field!(each, item, updated);
patch!(each, item, option); patch_opt_field!(each, item, option);
self.items = Some(items); self.items = Some(items);
return self.save_file().await; return self.save_file().await;
@@ -420,6 +412,21 @@ pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file
profiles_append_item_safe(item).await profiles_append_item_safe(item).await
} }
/// Applies `edit` to the committed profiles; commits only when the edit succeeds.
async fn with_profiles_edit<F, Fut>(edit: F) -> Result<()>
where
F: FnOnce(IProfiles) -> Fut + Send,
Fut: std::future::Future<Output = Result<IProfiles>> + Send,
{
Config::profiles()
.await
.with_data_modify(|profiles| async move {
let profiles = edit(profiles).await?;
Ok((profiles, ()))
})
.await
}
pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> { pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> {
let profiles = Config::profiles().await; let profiles = Config::profiles().await;
profiles_append_item_to_safe(&profiles, item).await profiles_append_item_to_safe(&profiles, item).await
@@ -435,43 +442,19 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft<IProfiles>, it
} }
pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> { pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> {
Config::profiles() with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await
.await
.with_data_modify(|mut profiles| async move {
profiles.patch_item(index, item).await?;
Ok((profiles, ()))
})
.await
} }
pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> { pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> {
Config::profiles() with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await
.await
.with_data_modify(|mut profiles| async move {
profiles.reorder(active_id, over_id).await?;
Ok((profiles, ()))
})
.await
} }
pub(crate) async fn profiles_save_file_safe() -> Result<()> { pub(crate) async fn profiles_save_file_safe() -> Result<()> {
Config::profiles() with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await
.await
.with_data_modify(|profiles| async move {
profiles.save_file().await?;
Ok((profiles, ()))
})
.await
} }
pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> { pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> {
Config::profiles() with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await
.await
.with_data_modify(|mut profiles| async move {
profiles.update_item(index, item).await?;
Ok((profiles, ()))
})
.await
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@@ -631,6 +614,53 @@ async fn select_node_with_timeout(group_name: &String, node: &String) -> Result<
.with_context(|| format!("failed to select node [{node}] for group [{group_name}]")) .with_context(|| format!("failed to select node [{node}] for group [{group_name}]"))
} }
// mihomo lists these first in GLOBAL, so an unpicked GLOBAL starts on DIRECT.
const GLOBAL_BUILTINS: [&str; 2] = ["DIRECT", "REJECT"];
/// GLOBAL's first visible group, else its first node, while GLOBAL is still on a built-in.
fn global_exit_target(proxies: &Proxies) -> Option<&str> {
let global = proxies.proxies.get("GLOBAL")?;
if !global.now.as_deref().is_some_and(|now| GLOBAL_BUILTINS.contains(&now)) {
return None;
}
let members = global.all.as_deref()?;
let is_visible_group = |name: &str| {
proxies.proxies.get(name).is_some_and(|proxy| {
proxy.hidden != Some(true)
&& matches!(
proxy.proxy_type,
ProxyType::Selector | ProxyType::URLTest | ProxyType::Fallback | ProxyType::LoadBalance
)
})
};
members
.iter()
.find(|name| is_visible_group(name))
.or_else(|| members.iter().find(|name| !GLOBAL_BUILTINS.contains(&name.as_str())))
.map(|name| name.as_str())
}
/// Moves GLOBAL off a built-in unless the profile records any GLOBAL choice.
pub(crate) async fn leave_default_global_exit() -> Result<()> {
let profiles = Config::profiles().await.data_arc();
let has_record = profiles
.current
.as_ref()
.and_then(|current| profiles.get_item(current).ok())
.and_then(|item| item.selected.as_deref())
.is_some_and(|selected| selected.iter().any(|item| item.name.as_deref() == Some("GLOBAL")));
if has_record {
return Ok(());
}
let snapshot = fetch_proxies_with_timeout().await?;
let Some(target) = global_exit_target(&snapshot) else {
return Ok(());
};
select_node_with_timeout(&String::from("GLOBAL"), &String::from(target)).await?;
logging!(info, Type::Config, "moved GLOBAL off its built-in default to {target}");
Ok(())
}
fn remaining_activations( fn remaining_activations(
activations: &[(String, String)], activations: &[(String, String)],
completed: &HashMap<String, String>, completed: &HashMap<String, String>,
@@ -713,19 +743,21 @@ where
Ok(()) Ok(())
} }
fn current_item_mut(profiles: &mut IProfiles) -> Option<&mut PrfItem> {
let current = profiles.current.clone()?;
profiles
.items
.as_mut()?
.iter_mut()
.find(|item| item.uid.as_ref() == Some(&current))
}
/// Records a backend-made selection so the next core start restores it. /// Records a backend-made selection so the next core start restores it.
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> { pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
let group_name = String::from(group_name); let group_name = String::from(group_name);
let node = String::from(node); let node = String::from(node);
persist_selected_update(true, move |mut profiles| async move { persist_selected_update(true, move |mut profiles| async move {
let Some(current) = profiles.current.clone() else { let Some(item) = current_item_mut(&mut profiles) else {
return Ok((profiles, false));
};
let Some(item) = profiles
.items
.as_mut()
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(&current)))
else {
return Ok((profiles, false)); return Ok((profiles, false));
}; };
@@ -760,14 +792,7 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> { pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
persist_selected_update(true, move |mut profiles| async move { persist_selected_update(true, move |mut profiles| async move {
let Some(current) = profiles.current.clone() else { let Some(item) = current_item_mut(&mut profiles) else {
return Ok((profiles, false));
};
let Some(item) = profiles
.items
.as_mut()
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(&current)))
else {
return Ok((profiles, false)); return Ok((profiles, false));
}; };
@@ -1041,6 +1066,12 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
.clone() .clone()
.unwrap_or_default(); .unwrap_or_default();
if Config::clash().await.latest_arc().get_mode().as_deref() == Some("global")
&& let Err(err) = leave_default_global_exit().await
{
logging!(warn, Type::Config, "failed to move GLOBAL off its default: {err:#}");
}
if selected.is_empty() { if selected.is_empty() {
if is_activation_current(generation) { if is_activation_current(generation) {
announce(Refresh::Clash); announce(Refresh::Clash);
+76 -84
View File
@@ -1,6 +1,6 @@
use crate::config::Config; use crate::config::Config;
use crate::{ use crate::{
config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted},
constants::network, constants::network,
utils::{dirs, help}, utils::{dirs, help},
}; };
@@ -39,7 +39,7 @@ pub struct IVerge {
pub traffic_graph: Option<bool>, pub traffic_graph: Option<bool>,
/// show memory info (only for Clash Meta) /// show memory info (only for Mihomo)
pub enable_memory_usage: Option<bool>, pub enable_memory_usage: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -432,98 +432,90 @@ impl IVerge {
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn patch_config(&mut self, patch: &Self) { pub fn patch_config(&mut self, patch: &Self) {
macro_rules! patch { patch_opt_field!(self, patch, app_log_level);
($key: tt) => { patch_opt_field!(self, patch, app_log_max_size);
if patch.$key.is_some() { patch_opt_field!(self, patch, app_log_max_count);
self.$key = patch.$key.clone();
}
};
}
patch!(app_log_level); patch_opt_field!(self, patch, language);
patch!(app_log_max_size); patch_opt_field!(self, patch, theme_mode);
patch!(app_log_max_count); patch_opt_field!(self, patch, tray_event);
patch_opt_field!(self, patch, env_type);
patch!(language); patch_opt_field!(self, patch, start_page);
patch!(theme_mode); patch_opt_field!(self, patch, startup_script);
patch!(tray_event); patch_opt_field!(self, patch, traffic_graph);
patch!(env_type); patch_opt_field!(self, patch, enable_memory_usage);
patch!(start_page); patch_opt_field!(self, patch, enable_group_icon);
patch!(startup_script); patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur);
patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_group_icon);
patch!(pause_render_traffic_stats_on_blur);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
patch!(tray_icon); patch_opt_field!(self, patch, tray_icon);
patch!(menu_icon); patch_opt_field!(self, patch, menu_icon);
patch!(menu_order); patch_opt_field!(self, patch, menu_order);
patch!(proxy_group_tools_position); patch_opt_field!(self, patch, proxy_group_tools_position);
patch!(notice_position); patch_opt_field!(self, patch, notice_position);
patch!(collapse_navbar); patch_opt_field!(self, patch, collapse_navbar);
patch!(common_tray_icon); patch_opt_field!(self, patch, common_tray_icon);
patch!(sysproxy_tray_icon); patch_opt_field!(self, patch, sysproxy_tray_icon);
patch!(tun_tray_icon); patch_opt_field!(self, patch, tun_tray_icon);
patch!(enable_tun_mode); patch_opt_field!(self, patch, enable_tun_mode);
patch!(enable_auto_launch); patch_opt_field!(self, patch, enable_auto_launch);
patch!(enable_silent_start); patch_opt_field!(self, patch, enable_silent_start);
patch!(enable_hover_jump_navigator); patch_opt_field!(self, patch, enable_hover_jump_navigator);
patch!(hover_jump_navigator_delay); patch_opt_field!(self, patch, hover_jump_navigator_delay);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
patch!(verge_redir_port); patch_opt_field!(self, patch, verge_redir_port);
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
patch!(verge_redir_enabled); patch_opt_field!(self, patch, verge_redir_enabled);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
patch!(verge_tproxy_port); patch_opt_field!(self, patch, verge_tproxy_port);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
patch!(verge_tproxy_enabled); patch_opt_field!(self, patch, verge_tproxy_enabled);
patch!(verge_mixed_port); patch_opt_field!(self, patch, verge_mixed_port);
patch!(verge_socks_port); patch_opt_field!(self, patch, verge_socks_port);
patch!(verge_socks_enabled); patch_opt_field!(self, patch, verge_socks_enabled);
patch!(verge_port); patch_opt_field!(self, patch, verge_port);
patch!(verge_http_enabled); patch_opt_field!(self, patch, verge_http_enabled);
patch!(enable_system_proxy); patch_opt_field!(self, patch, enable_system_proxy);
patch!(enable_proxy_guard); patch_opt_field!(self, patch, enable_proxy_guard);
patch!(enable_bypass_check); patch_opt_field!(self, patch, enable_bypass_check);
patch!(use_default_bypass); patch_opt_field!(self, patch, use_default_bypass);
patch!(system_proxy_bypass); patch_opt_field!(self, patch, system_proxy_bypass);
patch!(proxy_guard_duration); patch_opt_field!(self, patch, proxy_guard_duration);
patch!(proxy_auto_config); patch_opt_field!(self, patch, proxy_auto_config);
patch!(pac_file_content); patch_opt_field!(self, patch, pac_file_content);
patch!(proxy_host); patch_opt_field!(self, patch, proxy_host);
patch!(theme_setting); patch_opt_field!(self, patch, theme_setting);
patch!(web_ui_list); patch_opt_field!(self, patch, web_ui_list);
patch!(clash_core); patch_opt_field!(self, patch, clash_core);
patch!(hotkeys); patch_opt_field!(self, patch, hotkeys);
patch!(enable_global_hotkey); patch_opt_field!(self, patch, enable_global_hotkey);
patch!(auto_close_connection); patch_opt_field!(self, patch, auto_close_connection);
patch!(auto_check_update); patch_opt_field!(self, patch, auto_check_update);
patch!(default_latency_test); patch_opt_field!(self, patch, default_latency_test);
patch!(default_latency_timeout); patch_opt_field!(self, patch, default_latency_timeout);
patch!(enable_auto_delay_detection); patch_opt_field!(self, patch, enable_auto_delay_detection);
patch!(auto_delay_detection_interval_minutes); patch_opt_field!(self, patch, auto_delay_detection_interval_minutes);
patch!(enable_builtin_enhanced); patch_opt_field!(self, patch, enable_builtin_enhanced);
patch!(proxy_layout_column); patch_opt_field!(self, patch, proxy_layout_column);
patch!(test_list); patch_opt_field!(self, patch, test_list);
patch!(auto_log_clean); patch_opt_field!(self, patch, auto_log_clean);
patch!(enable_auto_backup_schedule); patch_opt_field!(self, patch, enable_auto_backup_schedule);
patch!(auto_backup_interval_hours); patch_opt_field!(self, patch, auto_backup_interval_hours);
patch!(auto_backup_on_change); patch_opt_field!(self, patch, auto_backup_on_change);
patch!(webdav_url); patch_opt_field!(self, patch, webdav_url);
patch!(webdav_username); patch_opt_field!(self, patch, webdav_username);
patch!(webdav_password); patch_opt_field!(self, patch, webdav_password);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
patch!(enable_tray_speed); patch_opt_field!(self, patch, enable_tray_speed);
patch!(tray_proxy_groups_display_mode); patch_opt_field!(self, patch, tray_proxy_groups_display_mode);
patch!(tray_inline_outbound_modes); patch_opt_field!(self, patch, tray_inline_outbound_modes);
patch!(enable_auto_light_weight_mode); patch_opt_field!(self, patch, enable_auto_light_weight_mode);
patch!(auto_light_weight_minutes); patch_opt_field!(self, patch, auto_light_weight_minutes);
patch!(enable_dns_settings); patch_opt_field!(self, patch, enable_dns_settings);
patch!(home_cards); patch_opt_field!(self, patch, home_cards);
patch!(enable_external_controller); patch_opt_field!(self, patch, enable_external_controller);
} }
pub fn get_log_level(&self) -> LevelFilter { pub fn get_log_level(&self) -> LevelFilter {
+7
View File
@@ -14,6 +14,13 @@ pub mod network {
} }
} }
pub mod sysproxy {
/// Single source of the macOS default bypass list; both proxy backends compose from it.
#[cfg(target_os = "macos")]
pub const MACOS_DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
}
pub mod timing { pub mod timing {
use super::Duration; use super::Duration;
+7 -13
View File
@@ -7,11 +7,10 @@ use crate::{
use anyhow::Result; use anyhow::Result;
use chrono::Local; use chrono::Local;
use clash_verge_logging::{Type, logging}; use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use std::{ use std::{
path::PathBuf, path::PathBuf,
sync::{ sync::{
Arc, OnceLock, OnceLock,
atomic::{AtomicBool, AtomicI64, Ordering}, atomic::{AtomicBool, AtomicI64, Ordering},
}, },
time::{Duration, UNIX_EPOCH}, time::{Duration, UNIX_EPOCH},
@@ -82,8 +81,9 @@ impl Default for AutoBackupSettings {
} }
pub struct AutoBackupManager { pub struct AutoBackupManager {
settings: Arc<RwLock<AutoBackupSettings>>,
settings_tx: watch::Sender<AutoBackupSettings>, settings_tx: watch::Sender<AutoBackupSettings>,
// The watch channel is the single settings store; no duplicate copies.
settings_rx: watch::Receiver<AutoBackupSettings>,
runner_started: AtomicBool, runner_started: AtomicBool,
exec_lock: Mutex<()>, exec_lock: Mutex<()>,
last_backup: AtomicI64, last_backup: AtomicI64,
@@ -93,10 +93,10 @@ impl AutoBackupManager {
pub fn global() -> &'static Self { pub fn global() -> &'static Self {
static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new(); static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new();
INSTANCE.get_or_init(|| { INSTANCE.get_or_init(|| {
let (tx, _rx) = watch::channel(AutoBackupSettings::default()); let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default());
Self { Self {
settings: Arc::new(RwLock::new(AutoBackupSettings::default())), settings_tx,
settings_tx: tx, settings_rx,
runner_started: AtomicBool::new(false), runner_started: AtomicBool::new(false),
exec_lock: Mutex::new(()), exec_lock: Mutex::new(()),
last_backup: AtomicI64::new(0), last_backup: AtomicI64::new(0),
@@ -106,9 +106,6 @@ impl AutoBackupManager {
pub async fn init(&self) -> Result<()> { pub async fn init(&self) -> Result<()> {
let settings = Self::load_settings().await; let settings = Self::load_settings().await;
{
*self.settings.write() = settings;
}
let _ = self.settings_tx.send_replace(settings); let _ = self.settings_tx.send_replace(settings);
self.maybe_start_runner(settings); self.maybe_start_runner(settings);
Ok(()) Ok(())
@@ -116,9 +113,6 @@ impl AutoBackupManager {
pub async fn refresh_settings(&self) -> Result<()> { pub async fn refresh_settings(&self) -> Result<()> {
let settings = Self::load_settings().await; let settings = Self::load_settings().await;
{
*self.settings.write() = settings;
}
let _ = self.settings_tx.send_replace(settings); let _ = self.settings_tx.send_replace(settings);
self.maybe_start_runner(settings); self.maybe_start_runner(settings);
Ok(()) Ok(())
@@ -193,7 +187,7 @@ impl AutoBackupManager {
} }
async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> { async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> {
let snapshot = *self.settings.read(); let snapshot = *self.settings_rx.borrow();
if trigger.is_schedule() && !snapshot.schedule_enabled { if trigger.is_schedule() && !snapshot.schedule_enabled {
return Ok(()); return Ok(());
-9
View File
@@ -466,12 +466,3 @@ impl Hotkey {
(del_list, add_list) (del_list, add_list)
} }
} }
impl Drop for Hotkey {
fn drop(&mut self) {
let app_handle = handle::Handle::app_handle();
if let Err(e) = app_handle.global_shortcut().unregister_all() {
logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e);
}
}
}
+53 -42
View File
@@ -40,8 +40,47 @@ impl LightweightState {
static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8); static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8);
static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0); struct WindowListener {
static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0); handler_id: AtomicU32,
event: &'static str,
label: &'static str,
}
impl WindowListener {
fn setup(&self, on_event: impl Fn() + Send + 'static) {
if let Some(window) = WindowManager::get_main_window() {
let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel);
if previous_handler_id != 0 {
window.unlisten(previous_handler_id);
logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label);
}
let handler_id = window.listen(self.event, move |_event| on_event());
self.handler_id.store(handler_id, Ordering::Release);
}
}
fn cancel(&self) {
let id = self.handler_id.swap(0, Ordering::AcqRel);
if id != 0 {
if let Some(window) = WindowManager::get_main_window() {
window.unlisten(id);
}
logging!(debug, Type::Lightweight, "取消了{}监听", self.label);
}
}
}
static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener {
handler_id: AtomicU32::new(0),
event: "tauri://close-requested",
label: "窗口关闭",
};
static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener {
handler_id: AtomicU32::new(0),
event: "tauri://focus",
label: "窗口焦点",
};
static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None); static CANCEL_TX: Mutex<Option<oneshot::Sender<()>>> = Mutex::new(None);
@@ -154,55 +193,27 @@ pub async fn add_light_weight_timer() {
} }
fn setup_window_close_listener() { fn setup_window_close_listener() {
if let Some(window) = WindowManager::get_main_window() { WINDOW_CLOSE_LISTENER.setup(|| {
let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); std::mem::drop(AsyncHandler::spawn(|| async {
if previous_handler_id != 0 { setup_light_weight_timer().await;
window.unlisten(previous_handler_id); }));
logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听"); logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
} });
let handler_id = window.listen("tauri://close-requested", move |_event| {
std::mem::drop(AsyncHandler::spawn(|| async {
setup_light_weight_timer().await;
}));
logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时");
});
WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release);
}
} }
fn cancel_window_close_listener() { fn cancel_window_close_listener() {
let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); WINDOW_CLOSE_LISTENER.cancel();
if id != 0 {
if let Some(window) = WindowManager::get_main_window() {
window.unlisten(id);
}
logging!(debug, Type::Lightweight, "取消了窗口关闭监听");
}
} }
fn setup_webview_focus_listener() { fn setup_webview_focus_listener() {
if let Some(window) = WindowManager::get_main_window() { WEBVIEW_FOCUS_LISTENER.setup(|| {
let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); cancel_light_weight_timer();
if previous_handler_id != 0 { logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
window.unlisten(previous_handler_id); });
logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听");
}
let handler_id = window.listen("tauri://focus", move |_event| {
cancel_light_weight_timer();
logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时");
});
WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release);
}
} }
fn cancel_webview_focus_listener() { fn cancel_webview_focus_listener() {
let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); WEBVIEW_FOCUS_LISTENER.cancel();
if id != 0 {
if let Some(window) = WindowManager::get_main_window() {
window.unlisten(id);
}
logging!(debug, Type::Lightweight, "取消了窗口焦点监听");
}
} }
async fn setup_light_weight_timer() { async fn setup_light_weight_timer() {
+18 -6
View File
@@ -559,12 +559,13 @@ impl CoreManager {
})?; })?;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
crate::utils::resolve::dns::sync_public_dns().await; crate::utils::resolve::dns::sync_public_dns().await;
// At login the app usually beats the network. Leave the write to the network watcher // Only automatic restores can wait for the watcher; explicit requests must report failure.
// rather than fail — only if the watcher is actually live; a user toggling while
// offline still fails fast.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
if crate::core::network_watch::is_armed() let defer_until_network = crate::core::network_watch::is_armed()
&& !crate::utils::resolve::is_resolve_done() && crate::core::notification::what_was_asked()
== crate::core::notification::FailedOperation::SystemProxyRestore;
#[cfg(target_os = "macos")]
if defer_until_network
&& Config::verge() && Config::verge()
.await .await
.latest_arc() .latest_arc()
@@ -579,7 +580,18 @@ impl CoreManager {
); );
return Ok(()); return Ok(());
} }
proxy_control::apply().await?; if let Err(error) = proxy_control::apply().await {
#[cfg(target_os = "macos")]
if defer_until_network && proxy_control::is_missing_network_service_failure(&error) {
logging!(
debug,
Type::Core,
"network service disappeared; the system proxy waits for its return"
);
return Ok(());
}
return Err(error);
}
if !expectation.is_valid( if !expectation.is_valid(
self.get_running_mode().as_ref(), self.get_running_mode().as_ref(),
self.current_core_readiness_generation(), self.current_core_readiness_generation(),
+1
View File
@@ -49,6 +49,7 @@ async fn reapply() {
.enable_system_proxy .enable_system_proxy
.unwrap_or_default() .unwrap_or_default()
{ {
crate::utils::resolve::dns::sync_public_dns().await;
return; return;
} }
if !proxy_control::has_network_service().await { if !proxy_control::has_network_service().await {
+26 -14
View File
@@ -6,7 +6,7 @@ use crate::{
notification::{self, FailedOperation}, notification::{self, FailedOperation},
runstate::RUN_STATE, runstate::RUN_STATE,
service, service,
sysopt::Sysopt, sysopt::{Sysopt, format_bypass},
}, },
process::AsyncHandler, process::AsyncHandler,
utils::{ utils::{
@@ -180,6 +180,14 @@ pub const fn is_missing_network_service(_error: &sysproxy::Error) -> bool {
false false
} }
pub fn is_missing_network_service_failure(error: &anyhow::Error) -> bool {
!SystemProxyStateUnknown::is_in(error)
&& SysproxyFailure::from_chain(error).is_none()
&& error
.downcast_ref::<sysproxy::Error>()
.is_some_and(is_missing_network_service)
}
/// Distinguish a misplaced sidecar from a missing privileged service. /// Distinguish a misplaced sidecar from a missing privileged service.
async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error { async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error {
if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) { if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) {
@@ -204,8 +212,6 @@ const fn refusal_classification(service_usable: Option<bool>) -> SysproxyFailure
} }
const LOOPBACK_HOST: &str = "127.0.0.1"; const LOOPBACK_HOST: &str = "127.0.0.1";
const MACOS_DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
const MAX_SERVICE_BYPASS_LEN: usize = 8192; const MAX_SERVICE_BYPASS_LEN: usize = 8192;
static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new(); static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new();
@@ -327,13 +333,7 @@ fn service_bypass(verge: &IVerge) -> Result<String> {
let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); let custom = verge.system_proxy_bypass.as_deref().unwrap_or("");
ensure!(!custom.contains('\0'), "system proxy bypass contains NUL"); ensure!(!custom.contains('\0'), "system proxy bypass contains NUL");
let mut bypass = if custom.is_empty() { let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into();
MACOS_DEFAULT_BYPASS.to_owned()
} else if verge.use_default_bypass.unwrap_or(true) {
format!("{MACOS_DEFAULT_BYPASS},{custom}")
} else {
custom.to_owned()
};
truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN); truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN);
Ok(bypass) Ok(bypass)
} }
@@ -367,6 +367,13 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()), ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()),
// Direct fallback satisfies a disable request. // Direct fallback satisfies a disable request.
ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()), ProxyApplyOutcome::DirectFallback { .. } if matches!(requested, MacosProxyConfig::Disabled) => Ok(()),
// Older helpers encode a failed service lookup as fallback even when no OS write occurred.
#[cfg(target_os = "macos")]
ProxyApplyOutcome::DirectFallback { message }
if message == sysproxy::Error::NoActiveNetworkService.to_string() =>
{
Err(sysproxy::Error::NoActiveNetworkService.into())
}
ProxyApplyOutcome::DirectFallback { message } => { ProxyApplyOutcome::DirectFallback { message } => {
Err(SysproxyFailure::DirectFallback { detail: message }.into()) Err(SysproxyFailure::DirectFallback { detail: message }.into())
} }
@@ -617,6 +624,10 @@ pub async fn refresh_guard() -> Result<()> {
logging!(debug, Type::Core, "proxy guard superseded (generation {generation})"); logging!(debug, Type::Core, "proxy guard superseded (generation {generation})");
break; break;
} }
Err(error) if is_missing_network_service_failure(&error) => {
consecutive_failures = 0;
logging!(debug, Type::Core, "system proxy guard waits for a network service");
}
Err(error) => { Err(error) => {
consecutive_failures += 1; consecutive_failures += 1;
logging!( logging!(
@@ -1118,10 +1129,11 @@ mod tests {
.err() .err()
.unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail")); .unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail"));
assert_eq!( assert!(super::is_missing_network_service_failure(&error));
SysproxyFailure::from_chain(&error).map(SysproxyFailure::code), assert!(SysproxyFailure::from_chain(&error).is_none());
Some("SYSPROXY_DIRECT_FALLBACK") assert!(!super::is_missing_network_service_failure(
); &error.context(SystemProxyStateUnknown)
));
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+44 -41
View File
@@ -603,50 +603,71 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn uninstall_service() -> Result<()> { pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result<std::process::ExitStatus> {
logging!(info, Type::Service, "uninstall service");
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt as _; use std::os::windows::process::CommandExt as _;
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall.exe", || {
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
})?;
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let token = Token::with_current_process()?; let token = Token::with_current_process()?;
let level = token.privilege_level()?; let level = token.privilege_level()?;
let status = match level { let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?, PrivilegeLevel::NotPrivileged => {
_ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?, let mut command = RunasCommand::new(program);
if hide_window {
command.show(false);
}
command.status()?
}
_ => {
let mut command = StdCommand::new(program);
if hide_window {
// 0x08000000 is CREATE_NO_WINDOW, the direct counterpart of runas's show(false).
command.creation_flags(0x08000000);
}
command.status()?
}
}; };
Ok(status)
}
fn uninstaller_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result<PathBuf>) -> Result<PathBuf> {
let uninstall_path = packaged_service_tool_path(file_name, packaged_path)?;
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
Ok(uninstall_path)
}
fn ensure_uninstall_success(status: &std::process::ExitStatus) -> Result<()> {
if !status.success() { if !status.success() {
bail!( bail!(
"failed to uninstall service with status {}", "failed to uninstall service with status {}",
status.code().unwrap_or(-1) status.code().unwrap_or(-1)
); );
} }
Ok(()) Ok(())
} }
#[cfg(target_os = "windows")]
fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service");
let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall.exe", || {
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe"))
})?;
let status = run_maybe_elevated(&uninstall_path, true)?;
ensure_uninstall_success(&status)
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn uninstall_service() -> Result<()> { fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service"); logging!(info, Type::Service, "uninstall service");
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || { let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall")) Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall"))
})?; })?;
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let elevator = crate::utils::help::linux_elevator(); let elevator = crate::utils::help::linux_elevator();
let status = if linux_running_as_root() { let status = if linux_running_as_root() {
StdCommand::new(&uninstall_path).status()? StdCommand::new(&uninstall_path).status()?
@@ -676,14 +697,7 @@ fn uninstall_service() -> Result<()> {
status.code().unwrap_or(-1) status.code().unwrap_or(-1)
); );
if !status.success() { ensure_uninstall_success(&status)
bail!(
"failed to uninstall service with status {}",
status.code().unwrap_or(-1)
);
}
Ok(())
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -698,14 +712,10 @@ fn linux_running_as_root() -> bool {
fn uninstall_service() -> Result<()> { fn uninstall_service() -> Result<()> {
logging!(info, Type::Service, "uninstall service"); logging!(info, Type::Service, "uninstall service");
let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || { let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || {
Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall")) Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall"))
})?; })?;
if !uninstall_path.exists() {
bail!(format!("uninstaller not found: {uninstall_path:?}"));
}
let uninstall_path = macos_service_tool_path(&uninstall_path)?; let uninstall_path = macos_service_tool_path(&uninstall_path)?;
let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned(); let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned();
@@ -721,14 +731,7 @@ fn uninstall_service() -> Result<()> {
let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?; let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?;
if !status.success() { ensure_uninstall_success(&status)
bail!(
"failed to uninstall service with status {}",
status.code().unwrap_or(-1)
);
}
Ok(())
} }
fn install_service() -> Result<()> { fn install_service() -> Result<()> {
@@ -1967,7 +1970,7 @@ impl ServiceManager {
async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> { async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> {
run_operation_and_then(&RUN_STATE, operation, || async { run_operation_and_then(&RUN_STATE, operation, || async {
if let Err(error) = Tray::global().update_menu().await { if let Err(error) = Tray::global().update_part().await {
logging!( logging!(
warn, warn,
Type::Service, Type::Service,
+19 -14
View File
@@ -241,20 +241,19 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState
} }
pub(crate) struct Sysopt { pub(crate) struct Sysopt {
update_lock: TokioMutex<()>, // Owns the proxy target pair; every reader and writer goes through this lock.
proxy_state_lock: TokioMutex<(Sysproxy, Autoproxy)>,
guard_operation_lock: TokioMutex<()>, guard_operation_lock: TokioMutex<()>,
reset_sysproxy: AtomicBool, reset_sysproxy: AtomicBool,
inner_proxy: Arc<RwLock<(Sysproxy, Autoproxy)>>,
guard: Arc<RwLock<GuardMonitor>>, guard: Arc<RwLock<GuardMonitor>>,
} }
impl Default for Sysopt { impl Default for Sysopt {
fn default() -> Self { fn default() -> Self {
Self { Self {
update_lock: TokioMutex::new(()), proxy_state_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())),
guard_operation_lock: TokioMutex::new(()), guard_operation_lock: TokioMutex::new(()),
reset_sysproxy: AtomicBool::new(false), reset_sysproxy: AtomicBool::new(false),
inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))),
guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))),
} }
} }
@@ -269,10 +268,9 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172
#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "linux", target_os = "macos"))]
static BYPASS_SEPARATOR: &str = ","; static BYPASS_SEPARATOR: &str = ",";
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = static DEFAULT_BYPASS: &str = crate::constants::sysproxy::MACOS_DEFAULT_BYPASS;
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
fn format_bypass(use_default: bool, custom_bypass: &str) -> String { pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String {
if custom_bypass.is_empty() { if custom_bypass.is_empty() {
DEFAULT_BYPASS.into() DEFAULT_BYPASS.into()
} else if use_default { } else if use_default {
@@ -361,7 +359,7 @@ impl Sysopt {
} }
} }
while self.access_guard().read().get_state().is_pendding() { while self.access_guard().read().get_state().is_pendding() {
tokio::task::yield_now().await; tokio::time::sleep(Duration::from_millis(10)).await;
} }
// `start()` false may also mean a guard was already running. // `start()` false may also mean a guard was already running.
!self.access_guard().read().get_state().is_stopped() !self.access_guard().read().get_state().is_stopped()
@@ -409,18 +407,21 @@ impl Sysopt {
/// configuration instead of a partially-applied one (e.g. SOCKS already /// configuration instead of a partially-applied one (e.g. SOCKS already
/// disabled but HTTP still enabled mid-transition). /// disabled but HTTP still enabled mid-transition).
pub(crate) async fn wait_idle(&self) { pub(crate) async fn wait_idle(&self) {
let _ = self.update_lock.lock().await; let _ = self.proxy_state_lock.lock().await;
} }
/// init the sysproxy /// init the sysproxy
#[allow(
clippy::significant_drop_tightening,
reason = "the proxy state lock must stay held until the OS write finishes; wait_idle relies on it"
)]
pub(super) async fn update_sysproxy(&self) -> Result<()> { pub(super) async fn update_sysproxy(&self) -> Result<()> {
let _lock = self.update_lock.lock().await; let mut proxy_state = self.proxy_state_lock.lock().await;
let verge = Config::verge().await.latest_arc(); let verge = Config::verge().await.latest_arc();
// Configured, not live: this runs while the Core is being started or restarted, and // Configured, not live: this runs while the Core is being started or restarted, and
// asking a Core that is not up yet would only fall back here anyway. // asking a Core that is not up yet would only fall back here anyway.
let port = MixedPort::desired().await; let port = MixedPort::desired().await;
let pac_port = server::embedded_server_port()?; let pac_port = server::embedded_server_port()?;
// 先 await, 避免持有锁导致的 Send 问题
let bypass = get_bypass().await; let bypass = get_bypass().await;
let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( let (sys_enable, pac_enable, proxy_host, proxy_guard) = (
@@ -431,7 +432,7 @@ impl Sysopt {
); );
let (sys, auto, guard_type) = { let (sys, auto, guard_type) = {
let (sys, auto) = &mut *self.inner_proxy.write(); let (sys, auto) = &mut *proxy_state;
sys.host = proxy_host.into(); sys.host = proxy_host.into();
sys.port = port; sys.port = port;
sys.bypass = bypass.into(); sys.bypass = bypass.into();
@@ -528,6 +529,10 @@ impl Sysopt {
} }
/// reset the sysproxy /// reset the sysproxy
#[allow(
clippy::significant_drop_tightening,
reason = "the proxy state lock must stay held until the disable finishes; wait_idle relies on it"
)]
pub(super) async fn reset_sysproxy(&self) -> Result<()> { pub(super) async fn reset_sysproxy(&self) -> Result<()> {
if self if self
.reset_sysproxy .reset_sysproxy
@@ -539,13 +544,13 @@ impl Sysopt {
defer! { defer! {
self.reset_sysproxy.store(false, Ordering::SeqCst); self.reset_sysproxy.store(false, Ordering::SeqCst);
} }
let _lock = self.update_lock.lock().await; let mut proxy_state = self.proxy_state_lock.lock().await;
let _guard_operation = self.guard_operation_lock.lock().await; let _guard_operation = self.guard_operation_lock.lock().await;
let drained = self.stop_proxy_guard_locked().await; let drained = self.stop_proxy_guard_locked().await;
// 直接关闭所有代理 // 直接关闭所有代理
let (sys, auto) = { let (sys, auto) = {
let (sys, auto) = &mut *self.inner_proxy.write(); let (sys, auto) = &mut *proxy_state;
sys.enable = false; sys.enable = false;
auto.enable = false; auto.enable = false;
(sys.clone(), auto.clone()) (sys.clone(), auto.clone())
+2 -2
View File
@@ -7,6 +7,7 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error}; use clash_verge_logging::{Type, logging, logging_error};
use futures::StreamExt as _;
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
use smartstring::alias::String; use smartstring::alias::String;
use std::{ use std::{
@@ -21,7 +22,6 @@ use tokio::{
sync::mpsc, sync::mpsc,
time::{sleep, timeout}, time::{sleep, timeout},
}; };
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key}; use tokio_util::time::{DelayQueue, delay_queue::Key};
enum TimerCommand { enum TimerCommand {
@@ -501,6 +501,7 @@ impl Timer {
mod tests { mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition}; use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption}; use crate::config::{PrfItem, PrfOption};
use futures::StreamExt as _;
use parking_lot::Mutex; use parking_lot::Mutex;
use smartstring::alias::String; use smartstring::alias::String;
use std::collections::HashMap; use std::collections::HashMap;
@@ -513,7 +514,6 @@ mod tests {
time::Duration, time::Duration,
}; };
use tokio::sync::Barrier; use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue; use tokio_util::time::DelayQueue;
struct CancellationProbe { struct CancellationProbe {
+2 -1
View File
@@ -636,7 +636,8 @@ async fn create_tray_menu(
.tray_proxy_groups_display_mode .tray_proxy_groups_display_mode
.as_deref() .as_deref()
.unwrap_or("default"); .unwrap_or("default");
let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable"; let include_proxy_groups =
options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable");
let proxy_group_order_map = runtime_proxy_groups_order; let proxy_group_order_map = runtime_proxy_groups_order;
+28 -55
View File
@@ -18,6 +18,14 @@ pub struct SilentUpdater {
background_cancel: Mutex<Option<CancellationToken>>, background_cancel: Mutex<Option<CancellationToken>>,
} }
#[derive(Serialize, specta::Type)]
#[serde(rename_all = "snake_case")]
pub enum InstallUpdateOutcome {
Installed,
Cancelled,
UpdateChanged,
}
singleton!(SilentUpdater, SILENT_UPDATER); singleton!(SilentUpdater, SILENT_UPDATER);
impl SilentUpdater { impl SilentUpdater {
@@ -34,7 +42,7 @@ impl SilentUpdater {
} }
} }
#[derive(Serialize, Deserialize, specta::Type)] #[derive(Serialize, Deserialize)]
struct UpdateCacheMeta { struct UpdateCacheMeta {
version: String, version: String,
downloaded_at: String, downloaded_at: String,
@@ -101,11 +109,11 @@ impl SilentUpdater {
} }
/// Returns the cached installer for `update`, deleting a cache that does not match it. /// Returns the cached installer for `update`, deleting a cache that does not match it.
fn verified_cache(app_handle: &tauri::AppHandle, update: &Update) -> Option<Vec<u8>> { fn verified_cache(update: &Update) -> Option<Vec<u8>> {
let meta = Self::read_cache_meta().ok()?; let meta = Self::read_cache_meta().ok()?;
let bytes = if meta.version == update.version { let bytes = if meta.version == update.version {
Self::read_cache_bytes().and_then(|bytes| { Self::read_cache_bytes().and_then(|bytes| {
verify_signature(app_handle, &bytes, &update.signature, &update.version)?; update.verify(&bytes)?;
Ok(bytes) Ok(bytes)
}) })
} else { } else {
@@ -127,46 +135,6 @@ impl SilentUpdater {
} }
} }
/// The cache directory is user-writable and feeds an elevated installer, so it is re-verified
/// with the same rules `Update::download` applies.
fn verify_signature(app_handle: &tauri::AppHandle, bytes: &[u8], signature: &str, version: &str) -> Result<()> {
use base64::Engine as _;
let decode = |value: &str| -> Result<String> {
Ok(String::from_utf8(
base64::engine::general_purpose::STANDARD.decode(value)?,
)?)
};
let config: tauri_plugin_updater::Config = serde_json::from_value(
app_handle
.config()
.plugins
.0
.get("updater")
.cloned()
.context("updater is not configured")?,
)?;
let public_key = minisign_verify::PublicKey::decode(&decode(&config.pubkey)?)?;
let signature = minisign_verify::Signature::decode(&decode(signature)?)?;
public_key
.verify(bytes, &signature, true)
.context("signature mismatch")?;
match signature
.trusted_comment()
.split('\t')
.find_map(|field| field.strip_prefix("version:"))
{
Some(signed) => anyhow::ensure!(
signed.trim_start_matches('v') == version,
"signed for v{signed}, not v{version}"
),
None => anyhow::ensure!(!config.require_signed_version, "signature carries no version"),
}
Ok(())
}
pub fn is_build_to_stable(current: &str, remote: &str) -> bool { pub fn is_build_to_stable(current: &str, remote: &str) -> bool {
let Some((base, build)) = current.trim_start_matches('v').split_once('+') else { let Some((base, build)) = current.trim_start_matches('v').split_once('+') else {
return false; return false;
@@ -391,7 +359,7 @@ impl SilentUpdater {
} }
}; };
let Some(bytes) = Self::verified_cache(app_handle, &update) else { let Some(bytes) = Self::verified_cache(&update) else {
return false; return false;
}; };
@@ -424,13 +392,13 @@ impl SilentUpdater {
} }
/// Installs `version` for the update dialog, reusing the cache when it holds that version; /// Installs `version` for the update dialog, reusing the cache when it holds that version;
/// `false` when cancelled before the installer starts. /// A changed offer must be confirmed again before downloading or installing it.
pub async fn install_update( pub async fn install_update(
&self, &self,
app_handle: &tauri::AppHandle, app_handle: &tauri::AppHandle,
version: &str, version: &str,
on_event: impl Fn(DownloadEvent) + Sync, on_event: impl Fn(DownloadEvent) + Sync,
) -> Result<bool> { ) -> Result<InstallUpdateOutcome> {
let cancel = CancellationToken::new(); let cancel = CancellationToken::new();
*self.manual_cancel.lock() = Some(cancel.clone()); *self.manual_cancel.lock() = Some(cancel.clone());
let prepared = tokio::select! { let prepared = tokio::select! {
@@ -440,31 +408,36 @@ impl SilentUpdater {
// `cancel_download` cancels under the lock, so once the token is taken back its state is // `cancel_download` cancels under the lock, so once the token is taken back its state is
// final; a cancel during the last synchronous poll of `prepare_install` still wins. // final; a cancel during the last synchronous poll of `prepare_install` still wins.
self.manual_cancel.lock().take(); self.manual_cancel.lock().take();
let Some((update, bytes)) = prepared.filter(|_| !cancel.is_cancelled()).transpose()? else { let Some(prepared) = prepared.filter(|_| !cancel.is_cancelled()).transpose()? else {
logging!(info, Type::System, "Update v{version} cancelled"); logging!(info, Type::System, "Update v{version} cancelled");
return Ok(false); return Ok(InstallUpdateOutcome::Cancelled);
};
let Some((update, bytes)) = prepared else {
return Ok(InstallUpdateOutcome::UpdateChanged);
}; };
logging!(info, Type::System, "Installing update v{version}..."); logging!(info, Type::System, "Installing update v{version}...");
Self::install(update, bytes).await?; Self::install(update, bytes).await?;
Self::delete_cache(); Self::delete_cache();
Ok(true) Ok(InstallUpdateOutcome::Installed)
} }
async fn prepare_install( async fn prepare_install(
app_handle: &tauri::AppHandle, app_handle: &tauri::AppHandle,
version: &str, version: &str,
on_event: impl Fn(DownloadEvent) + Sync, on_event: impl Fn(DownloadEvent) + Sync,
) -> Result<(Update, Vec<u8>)> { ) -> Result<Option<(Update, Vec<u8>)>> {
let update = Self::check(app_handle) let Some(update) = Self::check(app_handle)
.await? .await?
.filter(|update| update.version == version) .filter(|update| update.version == version)
.with_context(|| format!("v{version} is no longer offered"))?; else {
let bytes = match Self::verified_cache(app_handle, &update) { return Ok(None);
};
let bytes = match Self::verified_cache(&update) {
Some(bytes) => bytes, Some(bytes) => bytes,
None => download(&update, on_event).await?, None => download(&update, on_event).await?,
}; };
Ok((update, bytes)) Ok(Some((update, bytes)))
} }
pub fn cancel_download(&self) { pub fn cancel_download(&self) {
@@ -641,7 +614,7 @@ impl SilentUpdater {
return Ok(()); return Ok(());
} }
if Self::verified_cache(app_handle, &update).is_some() { if Self::verified_cache(&update).is_some() {
logging!(info, Type::System, "Silent updater: v{version} already cached"); logging!(info, Type::System, "Silent updater: v{version} already cached");
} else { } else {
logging!(info, Type::System, "Silent updater: downloading v{version}..."); logging!(info, Type::System, "Silent updater: downloading v{version}...");
+2 -11
View File
@@ -1,10 +1,7 @@
#![cfg(target_os = "windows")] #![cfg(target_os = "windows")]
use crate::utils::dirs; use crate::{core::service, utils::dirs};
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
pub fn invoke_uwptools() -> Result<()> { pub fn invoke_uwptools() -> Result<()> {
let resource_dir = dirs::app_resources_dir()?; let resource_dir = dirs::app_resources_dir()?;
@@ -14,13 +11,7 @@ pub fn invoke_uwptools() -> Result<()> {
bail!("enableLoopback exe not found"); bail!("enableLoopback exe not found");
} }
let token = Token::with_current_process()?; service::run_maybe_elevated(&tool_path, false)?;
let level = token.privilege_level()?;
match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
_ => StdCommand::new(tool_path).status()?,
};
Ok(()) Ok(())
} }
@@ -1,8 +0,0 @@
// This function is exported for use by the Clash core
// eslint-disable-next-line unused-imports/no-unused-vars
function main(config, _name) {
if (config.mode === 'script') {
config.mode = 'rule'
}
return config
}
@@ -1,12 +0,0 @@
// This function is exported for use by the Clash core
// eslint-disable-next-line unused-imports/no-unused-vars
function main(config, _name) {
if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => {
if (p.type === 'hysteria' && typeof p.alpn === 'string') {
config.proxies[i].alpn = [p.alpn]
}
})
}
return config
}
+7 -67
View File
@@ -22,12 +22,6 @@ pub enum ChainType {
Groups(SeqMap), Groups(SeqMap),
} }
#[derive(Debug, Clone)]
pub enum ChainSupport {
ClashMeta,
ClashMetaAlpha,
}
// Helper trait to allow async conversion // Helper trait to allow async conversion
pub trait AsyncChainItemFrom { pub trait AsyncChainItemFrom {
async fn from_async(item: &PrfItem) -> Option<ChainItem>; async fn from_async(item: &PrfItem) -> Option<ChainItem>;
@@ -53,70 +47,16 @@ impl AsyncChainItemFrom for Option<ChainItem> {
uid, uid,
data: ChainType::Merge(help::read_mapping(&path).await.ok()?), data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
}), }),
"rules" => { "rules" | "proxies" | "groups" => {
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?; let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem { let data = match itype {
uid, "rules" => ChainType::Rules(seq_map),
data: ChainType::Rules(seq_map), "proxies" => ChainType::Proxies(seq_map),
}) _ => ChainType::Groups(seq_map),
} };
"proxies" => { Some(ChainItem { uid, data })
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Proxies(seq_map),
})
}
"groups" => {
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
Some(ChainItem {
uid,
data: ChainType::Groups(seq_map),
})
} }
_ => None, _ => None,
} }
} }
} }
impl ChainItem {
/// 内建支持一些脚本
pub fn builtin() -> Vec<(ChainSupport, Self)> {
// meta 的一些处理
let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
// meta 的一些处理
let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
vec![
(ChainSupport::ClashMeta, hy_alpn),
(ChainSupport::ClashMeta, meta_guard),
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
]
}
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
Self {
uid: uid.into(),
data: ChainType::Script(data.into()),
}
}
}
impl ChainSupport {
pub fn is_support(&self, core: Option<&String>) -> bool {
match core {
Some(core) => matches!(
(self, core.as_str()),
(Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha")
),
None => true,
}
}
}
+1 -12
View File
@@ -27,7 +27,7 @@ fn lowercase_key(key: &str) -> Value {
pub fn use_lowercase(config: &Mapping) -> Mapping { pub fn use_lowercase(config: &Mapping) -> Mapping {
let mut lowercased = Mapping::new(); let mut lowercased = Mapping::new();
for (key, value) in config.into_iter() { for (key, value) in config.iter() {
if let Some(key_str) = key.as_str() { if let Some(key_str) = key.as_str() {
lowercased.insert(lowercase_key(key_str), value.clone()); lowercased.insert(lowercase_key(key_str), value.clone());
} }
@@ -35,17 +35,6 @@ pub fn use_lowercase(config: &Mapping) -> Mapping {
lowercased lowercased
} }
pub fn use_lowercase_owned(config: Mapping) -> Mapping {
let mut lowercased = Mapping::new();
for (key, value) in config {
if let Some(key_str) = key.as_str() {
lowercased.insert(lowercase_key(key_str), value);
}
}
lowercased
}
pub fn use_sort(mut config: Mapping) -> Mapping { pub fn use_sort(mut config: Mapping) -> Mapping {
let mut sorted = Mapping::new(); let mut sorted = Mapping::new();
HANDLE_FIELDS.into_iter().for_each(|key| { HANDLE_FIELDS.into_iter().for_each(|key| {
+64 -127
View File
@@ -8,7 +8,7 @@ mod tun;
use self::{ use self::{
chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, chain::{AsyncChainItemFrom as _, ChainItem, ChainType},
field::{use_keys, use_lowercase_owned, use_sort}, field::{use_keys, use_lowercase, use_sort},
merge::use_merge, merge::use_merge,
script::use_script, script::use_script,
seq::{SeqMap, use_seq}, seq::{SeqMap, use_seq},
@@ -34,7 +34,6 @@ type ResultLog = Vec<(String, String)>;
#[derive(Debug)] #[derive(Debug)]
struct ConfigValues { struct ConfigValues {
clash_config: Mapping, clash_config: Mapping,
clash_core: Option<String>,
enable_tun: bool, enable_tun: bool,
enable_builtin: bool, enable_builtin: bool,
socks_enabled: bool, socks_enabled: bool,
@@ -128,8 +127,7 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
let dns_settings = verge_arc.dns_settings_for(profile_uid); let dns_settings = verge_arc.dns_settings_for(profile_uid);
let dns_override_confirmation = dns_settings.confirmation; let dns_override_confirmation = dns_settings.confirmation;
let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( let (enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = (
Some(verge_arc.get_valid_clash_core()),
enable_tun_mode.unwrap_or(false), enable_tun_mode.unwrap_or(false),
enable_builtin_enhanced.unwrap_or(true), enable_builtin_enhanced.unwrap_or(true),
verge_socks_enabled.unwrap_or(false), verge_socks_enabled.unwrap_or(false),
@@ -148,7 +146,6 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues {
ConfigValues { ConfigValues {
clash_config, clash_config,
clash_core,
enable_tun, enable_tun,
enable_builtin, enable_builtin,
socks_enabled, socks_enabled,
@@ -199,35 +196,25 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
let name = current_item.name.clone().unwrap_or_default(); let name = current_item.name.clone().unwrap_or_default();
let ProfileItems {
merge_item: default_merge,
script_item: default_script,
rules_item: default_rules,
proxies_item: default_proxies,
groups_item: default_groups,
global_merge: default_global_merge,
global_script: default_global_script,
..
} = ProfileItems::default();
let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!( let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!(
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem { chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,),
uid: "".into(), chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,),
data: ChainType::Merge(Mapping::new()), chain_item_or_default(profiles.get_item(&rules_uid).ok(), || default_rules,),
},), chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || default_proxies,),
chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem { chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,),
uid: "".into(), chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,),
},),
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
},),
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
},),
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
},),
chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
},),
chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},),
); );
Ok(ProfileItems { Ok(ProfileItems {
@@ -243,16 +230,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
}) })
} }
async fn process_global_items( async fn process_override_items(
mut config: Mapping, mut config: Mapping,
mut exists_keys: Vec<String>, mut exists_keys: Vec<String>,
mut result_map: HashMap<String, ResultLog>, mut result_map: HashMap<String, ResultLog>,
global_merge: ChainItem, merge_item: ChainItem,
global_script: ChainItem, script_item: ChainItem,
profile_name: &String, profile_name: &String,
authoritative: &AuthoritativeFields, authoritative: &AuthoritativeFields,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) { ) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Merge(merge) = global_merge.data { if let ChainType::Merge(merge) = merge_item.data {
exists_keys.extend(use_keys(&merge)); exists_keys.extend(use_keys(&merge));
let before = authoritative.current(&config); let before = authoritative.current(&config);
config = use_merge(&merge, config); config = use_merge(&merge, config);
@@ -262,11 +249,11 @@ async fn process_global_items(
.map(discarded_note) .map(discarded_note)
.collect(); .collect();
if !notes.is_empty() { if !notes.is_empty() {
result_map.entry(global_merge.uid).or_default().extend(notes); result_map.entry(merge_item.uid).or_default().extend(notes);
} }
} }
if let ChainType::Script(script) = global_script.data { if let ChainType::Script(script) = script_item.data {
let before = authoritative.current(&config); let before = authoritative.current(&config);
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await; let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
exists_keys.extend(changed_keys); exists_keys.extend(changed_keys);
@@ -277,7 +264,7 @@ async fn process_global_items(
.into_iter() .into_iter()
.map(discarded_note), .map(discarded_note),
); );
result_map.insert(global_script.uid, logs); result_map.insert(script_item.uid, logs);
} }
(config, exists_keys, result_map) (config, exists_keys, result_map)
@@ -544,46 +531,6 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping {
config config
} }
async fn process_profile_items(
mut config: Mapping,
mut exists_keys: Vec<String>,
mut result_map: HashMap<String, ResultLog>,
merge_item: ChainItem,
script_item: ChainItem,
profile_name: &String,
authoritative: &AuthoritativeFields,
) -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Merge(merge) = merge_item.data {
exists_keys.extend(use_keys(&merge));
let before = authoritative.current(&config);
config = use_merge(&merge, config);
let notes: ResultLog = authoritative
.overridden(&before, &authoritative.current(&config))
.into_iter()
.map(discarded_note)
.collect();
if !notes.is_empty() {
result_map.entry(merge_item.uid).or_default().extend(notes);
}
}
if let ChainType::Script(script) = script_item.data {
let before = authoritative.current(&config);
let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await;
exists_keys.extend(changed_keys);
config = res_config;
logs.extend(
authoritative
.overridden(&before, &authoritative.current(&config))
.into_iter()
.map(discarded_note),
);
result_map.insert(script_item.uid, logs);
}
(config, exists_keys, result_map)
}
/// Merges the app's clash config over the profile; switches are parameters to keep it pure. /// Merges the app's clash config over the profile; switches are parameters to keep it pure.
fn merge_default_config( fn merge_default_config(
mut config: Mapping, mut config: Mapping,
@@ -655,25 +602,12 @@ fn merge_default_config(
config config
} }
fn apply_builtin_scripts(mut config: Mapping, clash_core: Option<String>, enable_builtin: bool) -> Mapping { fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping {
if enable_builtin { if enable_builtin {
let items: Vec<_> = ChainItem::builtin() // The JS path saw a lowercased view; lowercase once, only when the builtin runs.
.into_iter() config = use_lowercase(&config);
.filter(|(s, _)| s.is_support(clash_core.as_ref())) logging!(debug, Type::Core, "run builtin script verge_hy_alpn");
.map(|(_, c)| c) config = builtin_hy_alpn(config);
.collect();
if !items.is_empty() {
// The JS path saw a lowercased view; lowercase once, only when a builtin runs.
config = use_lowercase_owned(config);
for item in items {
logging!(debug, Type::Core, "run builtin script {}", item.uid);
config = match item.uid.as_str() {
"verge_hy_alpn" => builtin_hy_alpn(config),
"verge_meta_guard" => builtin_meta_guard(config),
_ => config,
};
}
}
} }
config config
@@ -693,14 +627,6 @@ fn builtin_hy_alpn(mut config: Mapping) -> Mapping {
config config
} }
fn builtin_meta_guard(mut config: Mapping) -> Mapping {
if config.get("mode").and_then(Value::as_str) == Some("script") {
config.insert(Value::from("mode"), Value::from("rule"));
}
config
}
fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
// built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in // built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in
const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"]; const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"];
@@ -879,7 +805,6 @@ pub async fn enhance(
let cfg_vals = get_config_values(profile_uid).await; let cfg_vals = get_config_values(profile_uid).await;
let ConfigValues { let ConfigValues {
clash_config, clash_config,
clash_core,
enable_tun, enable_tun,
enable_builtin, enable_builtin,
socks_enabled, socks_enabled,
@@ -929,13 +854,13 @@ pub async fn enhance(
tproxy_enabled, tproxy_enabled,
); );
let config = apply_builtin_scripts(config, clash_core, enable_builtin); let config = apply_builtin_scripts(config, enable_builtin);
let config = use_tun(config, enable_tun); let config = use_tun(config, enable_tun);
let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await; let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await;
let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings); let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings);
let (config, exists_keys, result_map) = process_global_items( let (config, exists_keys, result_map) = process_override_items(
config, config,
exists_keys, exists_keys,
result_map, result_map,
@@ -946,7 +871,7 @@ pub async fn enhance(
) )
.await; .await;
let (config, exists_keys, result_map) = process_profile_items( let (config, exists_keys, result_map) = process_override_items(
config, config,
exists_keys, exists_keys,
result_map, result_map,
@@ -1428,7 +1353,7 @@ mod authoritative_field_tests {
mod tests { mod tests {
use super::{ use super::{
AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address, AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address,
process_global_items, process_profile_items, use_keys, process_override_items, use_keys,
}; };
use std::collections::HashMap; use std::collections::HashMap;
@@ -1476,22 +1401,28 @@ mod tests {
data: ChainType::Merge(merge.clone()), data: ChainType::Merge(merge.clone()),
}; };
let profile_name = "test-profile".into(); let profile_name = "test-profile".into();
let (config, keys, logs) = process_global_items( let (config, keys, logs) = process_override_items(
config, config,
vec![], vec![],
HashMap::new(), HashMap::new(),
merge_item("global-merge"), merge_item("global-merge"),
ChainItem::to_script("global-script", script), ChainItem {
uid: "global-script".into(),
data: ChainType::Script(script.into()),
},
&profile_name, &profile_name,
&authoritative, &authoritative,
) )
.await; .await;
let (config, _, logs) = process_profile_items( let (config, _, logs) = process_override_items(
config, config,
keys, keys,
logs, logs,
merge_item("profile-merge"), merge_item("profile-merge"),
ChainItem::to_script("profile-script", script), ChainItem {
uid: "profile-script".into(),
data: ChainType::Script(script.into()),
},
&profile_name, &profile_name,
&authoritative, &authoritative,
) )
@@ -1536,16 +1467,19 @@ mod tests {
nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}", nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}",
)), )),
}; };
let global_script = ChainItem::to_script( let global_script = ChainItem {
"Script", uid: "Script".into(),
r#"function main(config) { data: ChainType::Script(
r#"function main(config) {
config["global-script-wins"] = "global-script"; config["global-script-wins"] = "global-script";
config["profile-merge-wins"] = "global-script"; config["profile-merge-wins"] = "global-script";
config["profile-script-wins"] = "global-script"; config["profile-script-wins"] = "global-script";
config.nested.winner = "global-script"; config.nested.winner = "global-script";
return config; return config;
}"#, }"#
); .into(),
),
};
let profile_merge = ChainItem { let profile_merge = ChainItem {
uid: "profile-merge".into(), uid: "profile-merge".into(),
data: ChainType::Merge(mapping( data: ChainType::Merge(mapping(
@@ -1553,18 +1487,21 @@ mod tests {
nested: {winner: profile-merge}}", nested: {winner: profile-merge}}",
)), )),
}; };
let profile_script = ChainItem::to_script( let profile_script = ChainItem {
"profile-script", uid: "profile-script".into(),
r#"function main(config) { data: ChainType::Script(
r#"function main(config) {
config["profile-script-wins"] = "profile-script"; config["profile-script-wins"] = "profile-script";
config.nested.winner = "profile-script"; config.nested.winner = "profile-script";
return config; return config;
}"#, }"#
); .into(),
),
};
let profile_name = "test-profile".into(); let profile_name = "test-profile".into();
let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new()); let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new());
let (config, exists_keys, result_map) = process_global_items( let (config, exists_keys, result_map) = process_override_items(
config, config,
exists_keys, exists_keys,
HashMap::new(), HashMap::new(),
@@ -1574,7 +1511,7 @@ mod tests {
&authoritative, &authoritative,
) )
.await; .await;
let (config, exists_keys, _) = process_profile_items( let (config, exists_keys, _) = process_override_items(
config, config,
exists_keys, exists_keys,
result_map, result_map,
+3 -3
View File
@@ -1,6 +1,6 @@
use crate::{process::AsyncHandler, utils::tmpl}; use crate::{process::AsyncHandler, utils::tmpl};
use super::field::{use_lowercase, use_lowercase_owned}; use super::field::use_lowercase;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute}; use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute};
use clash_verge_logging::{Type, logging}; use clash_verge_logging::{Type, logging};
@@ -22,7 +22,7 @@ type EvalOutcome = (Result<Mapping, std::string::String>, Vec<(String, String)>)
pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult { pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult {
// The template is identity-plus-lowercasing; skip the Boa round-trip. // The template is identity-plus-lowercasing; skip the Boa round-trip.
if script == tmpl::ITEM_SCRIPT { if script == tmpl::ITEM_SCRIPT {
return (use_lowercase_owned(config), vec![], vec![]); return (use_lowercase(&config), vec![], vec![]);
} }
let json = match serde_json::to_string(&use_lowercase(&config)) { let json = match serde_json::to_string(&use_lowercase(&config)) {
@@ -173,7 +173,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome {
} }
match parse_json_safely(&result) { match parse_json_safely(&result) {
Ok(config) => (Ok(use_lowercase_owned(config)), outputs.lock().to_vec()), Ok(config) => (Ok(use_lowercase(&config)), outputs.lock().to_vec()),
Err(err) => { Err(err) => {
logging!( logging!(
error, error,
+1 -1
View File
@@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("fake-ip"); .unwrap_or("fake-ip");
if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) { if current_mode == "fake-ip" {
revise!(dns_val, "enable", true); revise!(dns_val, "enable", true);
revise!(dns_val, "ipv6", ipv6_val); revise!(dns_val, "ipv6", ipv6_val);
+51 -50
View File
@@ -33,6 +33,8 @@ async fn finalize_restored_verge_config(
webdav_username: Option<String>, webdav_username: Option<String>,
webdav_password: Option<String>, webdav_password: Option<String>,
) -> Result<()> { ) -> Result<()> {
let config_write = Config::lock_config_write().await;
// A broken restored `verge.yaml` is a restore failure, not a reason to load defaults. // A broken restored `verge.yaml` is a restore failure, not a reason to load defaults.
let mut restored = help::read_yaml::<IVerge>(&verge_path()?).await?; let mut restored = help::read_yaml::<IVerge>(&verge_path()?).await?;
restored.webdav_url = webdav_url; restored.webdav_url = webdav_url;
@@ -43,43 +45,42 @@ async fn finalize_restored_verge_config(
let restored_clash = IClashTemp::new().await; let restored_clash = IClashTemp::new().await;
let clash_draft = Config::clash().await; let clash_draft = Config::clash().await;
clash_draft.edit_draft(|d| { clash_draft.edit_draft(|d| {
*d = restored_clash.clone(); *d = restored_clash;
}); });
clash_draft.apply(); clash_draft.apply();
let restored_profiles = IProfiles::new().await; let restored_profiles = IProfiles::new().await;
let profiles_draft = Config::profiles().await; let profiles_draft = Config::profiles().await;
profiles_draft.edit_draft(|d| { profiles_draft.edit_draft(|d| {
*d = restored_profiles.clone(); *d = restored_profiles;
}); });
profiles_draft.apply(); profiles_draft.apply();
let verge_draft = Config::verge().await; let verge_draft = Config::verge().await;
{ verge_draft.edit_draft(|d| {
// Hold the write lock so a concurrent patch cannot stage a draft the proxy write reads. *d = restored.clone();
let _config_write = Config::lock_config_write().await; });
verge_draft.edit_draft(|d| { verge_draft.apply();
*d = restored.clone();
});
verge_draft.apply();
// Turn it off here; a failing core side effect below would otherwise skip this step. // Turn it off here; a failing core side effect below would otherwise skip this step.
if !restored.enable_system_proxy.unwrap_or_default() { if !restored.enable_system_proxy.unwrap_or_default() {
let result = async { let result = async {
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await; let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
proxy_control::apply().await?; proxy_control::apply().await?;
proxy_control::refresh_guard().await proxy_control::refresh_guard().await
} }
.await; .await;
if let Err(err) = result { if let Err(err) = result {
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}"); logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
if SystemProxyStateUnknown::is_in(&err) { if SystemProxyStateUnknown::is_in(&err) {
return Err(err); return Err(err);
}
} }
} }
} }
// `patch_verge` re-acquires the configuration write lock.
drop(config_write);
// Run configuration side effects without rewriting the already-restored file. // Run configuration side effects without rewriting the already-restored file.
if let Err(err) = super::patch_verge(&restored, true).await { if let Err(err) = super::patch_verge(&restored, true).await {
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}"); logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}");
@@ -133,14 +134,34 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
}) })
} }
/// Backup creation strips the WebDAV credentials, so the live ones are kept.
async fn restore_backup_archive(path: PathBuf) -> Result<()> {
let (webdav_url, webdav_username, webdav_password) = {
let verge = Config::verge().await;
let verge = verge.latest_arc();
(
verge.webdav_url.clone(),
verge.webdav_username.clone(),
verge.webdav_password.clone(),
)
};
let home_dir = app_home_dir()?;
AsyncHandler::spawn_blocking(move || -> Result<()> {
let file = std::fs::File::open(&path)?;
let mut zip = zip::ZipArchive::new(file)?;
zip.extract(&home_dir)?;
Ok(())
})
.await
.map_err(|join| anyhow!("backup extraction task failed: {join}"))??;
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await
}
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))] #[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
pub async fn restore_webdav_backup(filename: String) -> Result<()> { pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge().await;
let verge_data = verge.latest_arc();
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 backup_storage_path = app_home_dir() let backup_storage_path = app_home_dir()
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))? .map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
.join(filename.as_str()); .join(filename.as_str());
@@ -157,12 +178,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
err err
})?; })?;
let value = backup_storage_path.clone(); let res = restore_backup_archive(backup_storage_path.clone()).await;
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
let mut zip = zip::ZipArchive::new(file)?;
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
zip.extract(app_home_dir()?)?;
let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await;
let _ = backup_storage_path.remove_if_exists().await; let _ = backup_storage_path.remove_if_exists().await;
res res
} }
@@ -331,22 +347,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
return Err(anyhow!("Backup file not found: {}", filename)); return Err(anyhow!("Backup file not found: {}", filename));
} }
let (webdav_url, webdav_username, webdav_password) = { restore_backup_archive(target_path).await
let verge = Config::verge().await;
let verge = verge.latest_arc();
(
verge.webdav_url.clone(),
verge.webdav_username.clone(),
verge.webdav_password.clone(),
)
};
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
let mut zip = zip::ZipArchive::new(file)?;
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
zip.extract(app_home_dir()?)?;
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?;
Ok(())
} }
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))] #[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
+17 -45
View File
@@ -3,7 +3,7 @@ use crate::core::notify::{Refresh, announce};
use crate::{ use crate::{
config::{Config, MixedPort}, config::{Config, MixedPort},
core::{CoreManager, handle, tray}, core::{CoreManager, handle, tray},
feat::clean_async, feat::run_exit_cleanup_flow,
process::AsyncHandler, process::AsyncHandler,
utils, utils,
}; };
@@ -14,15 +14,8 @@ use smartstring::alias::String;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> = LazyLock::new(|| { static TLS_CONFIG: LazyLock<Arc<rustls::ClientConfig>> =
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions")));
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("Failed to set TLS versions")
.with_root_certificates(root_store)
.with_no_client_auth();
Arc::new(config)
});
pub async fn restart_clash_core() { pub async fn restart_clash_core() {
match CoreManager::global().restart_core().await { match CoreManager::global().restart_core().await {
@@ -38,38 +31,9 @@ pub async fn restart_clash_core() {
pub async fn restart_app() { pub async fn restart_app() {
logging!(debug, Type::System, "启动重启应用流程"); logging!(debug, Type::System, "启动重启应用流程");
handle::Handle::global().set_is_exiting(); let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await;
Config::apply_all_and_save_file().await;
let cleanup_result = clean_async().await;
logging!(
info,
Type::System,
"资源清理完成,退出代码: {}",
if cleanup_result.all_success { 0 } else { 1 }
);
if !cleanup_result.core_stopped { if !cleanup_result.core_stopped {
handle::Handle::global().clear_is_exiting();
// A failed stop may still have marked the core stopped, and exit cleanup skipped the DNS restore.
// A core that kept running keeps its DNS, whatever an unapplied config draft says.
#[cfg(target_os = "macos")]
{
let manager = CoreManager::global();
let _lifecycle = manager.lifecycle_lock.lock().await;
if matches!(
*manager.get_running_mode(),
crate::core::manager::RunningMode::NotRunning
) {
crate::utils::resolve::dns::sync_public_dns().await;
}
}
handle::Handle::notice(
NoticeStatus::AppRestartCoreStopFailed,
cleanup_result.stop_error.unwrap_or_default(),
);
return; return;
} }
@@ -102,12 +66,20 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
return Err(err.to_string().into()); return Err(err.to_string().into());
} }
let clash = Config::clash().await; if mode.as_str() == "global"
clash.edit_draft(|d| d.patch_config(&mapping)); && let Err(err) = crate::config::profiles::leave_default_global_exit().await
clash.apply(); {
logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}");
}
let clash_data = clash.data_arc(); let clash = Config::clash().await;
if clash_data.save_config().await.is_ok() { let save_result = {
let _config_write = Config::lock_config_write().await;
clash.edit_draft(|d| d.patch_config(&mapping));
clash.apply();
clash.data_arc().save_config().await
};
if save_result.is_ok() {
announce(Refresh::Clash); announce(Refresh::Clash);
tray::Tray::global().update_menu_and_icon().await; tray::Tray::global().update_menu_and_icon().await;
} }
-15
View File
@@ -47,26 +47,11 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) {
record_switched_node(group_name, proxy_name).await; record_switched_node(group_name, proxy_name).await;
announce(Refresh::Proxies); announce(Refresh::Proxies);
let _ = tray::Tray::global().update_menu().await; let _ = tray::Tray::global().update_menu().await;
return;
} }
Err(err) => { Err(err) => {
logging!(error, Type::Tray, "切换代理失败: {err:?}"); logging!(error, Type::Tray, "切换代理失败: {err:?}");
} }
} }
match handle::Handle::mihomo()
.select_node_for_group(group_name, proxy_name)
.await
{
Ok(_) => {
logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name);
record_switched_node(group_name, proxy_name).await;
let _ = tray::Tray::global().update_menu().await;
}
Err(err) => {
logging!(error, Type::Tray, "代理切换最终失败: {err:?}");
}
}
} }
async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> { async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result<Option<(String, Option<PrfOption>)>> {
+10 -5
View File
@@ -130,9 +130,7 @@ pub async fn open_or_close_dashboard() {
logging!(info, Type::Window, "Window toggle result: {result:?}"); logging!(info, Type::Window, "Window toggle result: {result:?}");
} }
pub async fn quit() { pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult {
logging!(debug, Type::System, "启动退出流程");
// 设置退出标志
handle::Handle::global().set_is_exiting(); handle::Handle::global().set_is_exiting();
Config::apply_all_and_save_file().await; Config::apply_all_and_save_file().await;
@@ -162,11 +160,18 @@ pub async fn quit() {
} }
} }
handle::Handle::notice( handle::Handle::notice(
NoticeStatus::AppQuitCoreStopFailed, stop_failed_notice,
cleanup_result.stop_error.unwrap_or_default(), cleanup_result.stop_error.clone().unwrap_or_default(),
); );
} }
cleanup_result
}
pub async fn quit() {
logging!(debug, Type::System, "启动退出流程");
let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppQuitCoreStopFailed).await;
utils::server::shutdown_embedded_server(); utils::server::shutdown_embedded_server();
let app_handle = handle::Handle::app_handle(); let app_handle = handle::Handle::app_handle();
app_handle.exit(if cleanup_result.all_success { 0 } else { 1 }); app_handle.exit(if cleanup_result.all_success { 0 } else { 1 });
-1
View File
@@ -54,7 +54,6 @@ mod app_init {
.build(), .build(),
) )
.plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
+2 -5
View File
@@ -354,7 +354,6 @@ pub(super) async fn init_dns_config() -> Result<()> {
Value::String("*.arpa".into()), Value::String("*.arpa".into()),
Value::String("time.*.com".into()), Value::String("time.*.com".into()),
Value::String("ntp.*.com".into()), Value::String("ntp.*.com".into()),
Value::String("time.*.com".into()),
Value::String("+.market.xiaomi.com".into()), Value::String("+.market.xiaomi.com".into()),
Value::String("localhost.ptlogin2.qq.com".into()), Value::String("localhost.ptlogin2.qq.com".into()),
Value::String("*.msftncsi.com".into()), Value::String("*.msftncsi.com".into()),
@@ -509,11 +508,9 @@ pub async fn init_resources() -> Result<()> {
let app_dir = dirs::app_home_dir()?; let app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir()?; let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() { // app_home is created by `ensure_directories` in `init_config`, which always runs first.
std::mem::drop(fs::create_dir_all(&app_dir).await);
}
if !res_dir.exists() { if !res_dir.exists() {
std::mem::drop(fs::create_dir_all(&res_dir).await); fs::create_dir_all(&res_dir).await?;
} }
let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"]; let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"];
+41 -41
View File
@@ -48,6 +48,17 @@ enum TlsRootMode {
StaticWebpkiRoots, StaticWebpkiRoots,
} }
/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer.
pub(crate) fn build_webpki_rooted_tls_config() -> Result<rustls::ClientConfig> {
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(config)
}
pub struct NetworkManager; pub struct NetworkManager;
impl NetworkManager { impl NetworkManager {
@@ -102,13 +113,7 @@ impl NetworkManager {
} }
fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> { fn build_static_webpki_tls_config() -> Result<rustls::ClientConfig> {
let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); let mut config = build_webpki_rooted_tls_config()?;
let mut config =
rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()?
.with_root_certificates(root_store)
.with_no_client_auth();
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Ok(config) Ok(config)
@@ -155,6 +160,23 @@ impl NetworkManager {
detail.contains("protocolversion") || detail.contains("protocol version") detail.contains("protocolversion") || detail.contains("protocol version")
} }
/// `allow_fallback` stays false when the caller already disabled certificate verification.
async fn request_with_static_webpki_fallback<T, F, Fut>(allow_fallback: bool, mut request: F) -> Result<T>
where
F: FnMut(TlsRootMode) -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
match request(TlsRootMode::PlatformVerifier).await {
Ok(value) => Ok(value),
Err(err) if allow_fallback && Self::should_retry_with_static_webpki_roots(&err) => {
request(TlsRootMode::StaticWebpkiRoots).await.map_err(|fallback_err| {
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed")
})
}
Err(err) => Err(err),
}
}
async fn get_with_tls_mode( async fn get_with_tls_mode(
&self, &self,
url: &str, url: &str,
@@ -264,34 +286,21 @@ impl NetworkManager {
user_agent: Option<String>, user_agent: Option<String>,
accept_invalid_certs: bool, accept_invalid_certs: bool,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let platform_result = self Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| {
.get_with_tls_mode( let user_agent = user_agent.clone();
url, async move {
proxy_type, self.get_with_tls_mode(
timeout_secs,
user_agent.clone(),
accept_invalid_certs,
TlsRootMode::PlatformVerifier,
)
.await;
match platform_result {
Ok(response) => Ok(response),
Err(err) if !accept_invalid_certs && Self::should_retry_with_static_webpki_roots(&err) => self
.get_with_tls_mode(
url, url,
proxy_type, proxy_type,
timeout_secs, timeout_secs,
user_agent, user_agent,
accept_invalid_certs, accept_invalid_certs,
TlsRootMode::StaticWebpkiRoots, tls_root_mode,
) )
.await .await
.map_err(|fallback_err| { }
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") })
}), .await
Err(err) => Err(err),
}
} }
async fn get_bytes_with_tls_mode( async fn get_bytes_with_tls_mode(
@@ -340,19 +349,10 @@ impl NetworkManager {
timeout_secs: Option<u64>, timeout_secs: Option<u64>,
max_bytes: usize, max_bytes: usize,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let platform_result = self Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move {
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier) self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode)
.await;
match platform_result {
Ok(body) => Ok(body),
Err(err) if Self::should_retry_with_static_webpki_roots(&err) => self
.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::StaticWebpkiRoots)
.await .await
.map_err(|fallback_err| { })
fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") .await
}),
Err(err) => Err(err),
}
} }
} }
+24 -54
View File
@@ -4,16 +4,14 @@ use serde_yaml_ng::{Mapping, Value};
use std::{ use std::{
io::Read as _, io::Read as _,
os::unix::process::CommandExt as _, os::unix::process::CommandExt as _,
path::{Path, PathBuf}, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
const DNS_STATE_FILE: &str = ".original_dns.txt";
const DNS_BACKUP_DIR: &str = ".original_dns";
// Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind. // Any address routed into TUN works, since dns-hijack answers it; the value only matters if left behind.
const PLACEHOLDER_DNS: &str = "114.114.114.114"; const PLACEHOLDER_DNS: &str = "114.114.114.114\n223.5.5.5\n1.1.1.1";
// Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit. // Scripts run under lifecycle_lock, and networksetup waits for other preference writers without a limit.
pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -26,14 +24,6 @@ fn dns_state_dir() -> anyhow::Result<PathBuf> {
Ok(dir) Ok(dir)
} }
fn restore_dns_state_dir(resource_dir: &Path, state_dir: PathBuf) -> PathBuf {
if resource_dir.join(DNS_STATE_FILE).exists() || resource_dir.join(DNS_BACKUP_DIR).exists() {
resource_dir.to_path_buf()
} else {
state_dir
}
}
// Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed. // Without the placeholder, a LAN resolver is reached outside TUN and fake-ip is bypassed.
fn needs_public_dns(config: &Mapping) -> bool { fn needs_public_dns(config: &Mapping) -> bool {
let section = |name: &str| config.get(name).and_then(Value::as_mapping); let section = |name: &str| config.get(name).and_then(Value::as_mapping);
@@ -79,25 +69,19 @@ pub async fn restore_public_dns() -> anyhow::Result<()> {
async fn set_public_dns() { async fn set_public_dns() {
logging!(debug, Type::Config, "try to set system dns"); logging!(debug, Type::Config, "try to set system dns");
let result = async { let result = run_dns_script("set_dns.sh", true).await;
let script = crate::utils::dirs::app_resources_dir()?.join("set_dns.sh");
run_dns_script(&script, dns_state_dir()?).await
}
.await;
match result { match result {
Ok(()) => logging!(info, Type::Config, "set system dns successfully"), Ok(DnsScriptOutcome::Complete) => logging!(info, Type::Config, "set system dns successfully"),
Ok(DnsScriptOutcome::Deferred) => {
logging!(debug, Type::Config, "system DNS waits for a primary network service")
}
Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"), Err(err) => logging!(error, Type::Config, "set system dns failed: {err:#}"),
} }
} }
async fn unset_public_dns() -> anyhow::Result<()> { async fn unset_public_dns() -> anyhow::Result<()> {
logging!(debug, Type::Config, "try to unset system dns"); logging!(debug, Type::Config, "try to unset system dns");
let result = async { let result = run_dns_script("unset_dns.sh", false).await.map(|_| ());
let resource_dir = crate::utils::dirs::app_resources_dir()?;
let state_dir = restore_dns_state_dir(&resource_dir, dns_state_dir()?);
run_dns_script(&resource_dir.join("unset_dns.sh"), state_dir).await
}
.await;
match &result { match &result {
Ok(()) => logging!(info, Type::Config, "unset system dns successfully"), Ok(()) => logging!(info, Type::Config, "unset system dns successfully"),
Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"), Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"),
@@ -105,12 +89,21 @@ async fn unset_public_dns() -> anyhow::Result<()> {
result result
} }
enum DnsScriptOutcome {
Complete,
Deferred,
}
/// On timeout the script's whole process group is killed, so no networksetup it started writes late. /// On timeout the script's whole process group is killed, so no networksetup it started writes late.
async fn run_dns_script(script: &Path, dir: PathBuf) -> anyhow::Result<()> { async fn run_dns_script(script_name: &str, allow_deferred: bool) -> anyhow::Result<DnsScriptOutcome> {
let resource_dir = crate::utils::dirs::app_resources_dir()?;
let script = resource_dir.join(script_name);
let dir = dns_state_dir()?;
ensure!(script.exists(), "DNS script not found: {}", script.display()); ensure!(script.exists(), "DNS script not found: {}", script.display());
let mut child = Command::new("bash") let mut child = Command::new("bash")
.arg(script) .arg(&script)
.arg(PLACEHOLDER_DNS) .arg(PLACEHOLDER_DNS)
.arg(resource_dir)
.current_dir(dir) .current_dir(dir)
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@@ -134,41 +127,18 @@ async fn run_dns_script(script: &Path, dir: PathBuf) -> anyhow::Result<()> {
if let Some(mut pipe) = child.stderr.take() { if let Some(mut pipe) = child.stderr.take() {
pipe.read_to_string(&mut stderr)?; pipe.read_to_string(&mut stderr)?;
} }
// EX_TEMPFAIL is reserved by set_dns.sh for a network service that is not ready.
if allow_deferred && status.code() == Some(75) {
return Ok(DnsScriptOutcome::Deferred);
}
ensure!( ensure!(
status.success(), status.success(),
"exit code {}: {}", "exit code {}: {}",
status.code().unwrap_or(-1), status.code().unwrap_or(-1),
stderr.trim() stderr.trim()
); );
Ok(()) Ok(DnsScriptOutcome::Complete)
}) })
.await .await
.context("DNS script task failed")? .context("DNS script task failed")?
} }
#[cfg(test)]
mod tests {
#[test]
#[allow(clippy::expect_used)]
fn restore_dns_state_dir_defaults_to_app_data_but_honors_legacy_file() {
use super::restore_dns_state_dir;
let root = std::env::temp_dir().join(format!("clash-verge-dns-{}", nanoid::nanoid!()));
let resource_dir = root.join("resources");
let state_dir = root.join("app-data");
std::fs::create_dir_all(&resource_dir).expect("create test resource directory");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), state_dir);
std::fs::write(resource_dir.join(".original_dns.txt"), "empty").expect("create legacy DNS state");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir.clone()), resource_dir);
// A migrated legacy backup whose restore failed must still be retried there.
std::fs::remove_file(resource_dir.join(".original_dns.txt")).expect("remove legacy DNS state");
std::fs::create_dir(resource_dir.join(".original_dns")).expect("create migrated DNS state");
assert_eq!(restore_dns_state_dir(&resource_dir, state_dir), resource_dir);
std::fs::remove_dir_all(root).expect("remove test directory");
}
}
-14
View File
@@ -1,8 +1,6 @@
use crate::core::notify::NoticeStatus; use crate::core::notify::NoticeStatus;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Result;
use crate::{ use crate::{
config::Config, config::Config,
core::{ core::{
@@ -97,18 +95,6 @@ async fn resolve_setup() {
resolve_done(); resolve_done();
} }
pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
CoreManager::global().stop_core().await?;
#[cfg(target_os = "macos")]
{
use dns::restore_public_dns;
restore_public_dns().await?;
}
Ok(())
}
fn init_scheme() { fn init_scheme() {
logging_error!(Type::Setup, init::init_scheme()); logging_error!(Type::Setup, init::init_scheme());
} }
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "2.5.10", "version": "2.6.0",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": { "bundle": {
"active": true, "active": true,
@@ -33,9 +33,9 @@
"updater": { "updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK", "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json"
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
], ],
"windows": { "windows": {
"installMode": "passive" "installMode": "passive"
+2 -1
View File
@@ -26,8 +26,9 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json" "https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+2 -1
View File
@@ -26,8 +26,9 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json" "https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+2 -1
View File
@@ -26,8 +26,9 @@
"active": true, "active": true,
"dialog": false, "dialog": false,
"endpoints": [ "endpoints": [
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json",
"https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json", "https://update.hwdns.net/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2.json" "https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-fixed-webview2-proxy.json"
], ],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
} }
+24
View File
@@ -21,3 +21,27 @@ These rules supplement the repository's root `AGENTS.md`.
- **Command ownership.** `services` owns backend command invocation and transport - **Command ownership.** `services` owns backend command invocation and transport
wrappers. Components and hooks use those boundaries; plugin mutations follow wrappers. Components and hooks use those boundaries; plugin mutations follow
the same intent ownership as backend commands. the same intent ownership as backend commands.
- **State liveness.** Every `useState`/`useRef` member has a reachable write
that differs from its initial value and a read that observes the difference.
A value that can only ever hold its initial value is a constant: declare the
constant and delete the state. A ref nothing reads is deleted, not kept.
- **Surface liveness.** Anything a module exposes — props, parameters, context
members, imperative-handle methods — ships with at least one external caller
in the same change. "Reserved for future use" fields, `_`-prefixed params
that callers still pass, and error or callback channels that every receiver
ignores are removed, not maintained.
- **Path liveness.** A fallback or branch names the concrete runtime condition
that reaches it. If no supported runtime can reach it, the branch is not
added — or it replaces the primary path. Constant subexpressions and cleanup
of resources that cannot exist yet are simplified away.
- **Effect economy.** One user interaction produces one state write and one
command. A handler does not re-apply a value its wrapper already applied,
and an effect does not re-run a refresh another branch of the same effect
just ran.
- **Mount economy.** Components that issue commands or subscriptions on mount
are mounted only where their UI is reachable. A dialog no rendered control
can open stays unmounted, not mounted-and-idle.
- **Resource bounds.** Every cache key, timer and subscription a feature
allocates is reused or released on refresh; refresh never allocates an
unbounded sequence of new keys. Retries re-create per-attempt resources so
a retry can actually succeed.
+13 -30
View File
@@ -22,23 +22,16 @@ interface BaseSplitChipEditorProps {
error?: boolean error?: boolean
helperText?: ReactNode helperText?: ReactNode
placeholder?: string placeholder?: string
rows?: number
separator?: string separator?: string
splitPattern?: RegExp
defaultMode?: BaseSplitChipEditorMode
showModeToggle?: boolean
ariaLabel?: string ariaLabel?: string
addLabel?: ReactNode
emptyLabel?: ReactNode
modeLabels?: Partial<Record<BaseSplitChipEditorMode, ReactNode>>
renderHeader?: (modeToggle: ReactNode) => ReactNode renderHeader?: (modeToggle: ReactNode) => ReactNode
} }
const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/ const DEFAULT_SPLIT_PATTERN = /[,\n;\r]+/
const splitValue = (value: string, splitPattern: RegExp) => const splitValue = (value: string) =>
value value
.split(splitPattern) .split(DEFAULT_SPLIT_PATTERN)
.map((item) => item.trim()) .map((item) => item.trim())
.filter(Boolean) .filter(Boolean)
@@ -49,35 +42,25 @@ export const BaseSplitChipEditor = ({
error = false, error = false,
helperText, helperText,
placeholder, placeholder,
rows = 4,
separator = ',', separator = ',',
splitPattern = DEFAULT_SPLIT_PATTERN,
defaultMode = 'visual',
showModeToggle = true,
ariaLabel, ariaLabel,
addLabel,
emptyLabel,
modeLabels,
renderHeader, renderHeader,
}: BaseSplitChipEditorProps) => { }: BaseSplitChipEditorProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [mode, setMode] = useState<BaseSplitChipEditorMode>(defaultMode) const [mode, setMode] = useState<BaseSplitChipEditorMode>('visual')
const [draft, setDraft] = useState('') const [draft, setDraft] = useState('')
const resolvedLabels = useMemo( const resolvedLabels = useMemo(
() => ({ () => ({
visual: modeLabels?.visual ?? t('shared.editorModes.visualization'), visual: t('shared.editorModes.visualization'),
advanced: modeLabels?.advanced ?? t('shared.editorModes.advanced'), advanced: t('shared.editorModes.advanced'),
add: addLabel ?? t('shared.actions.new'), add: t('shared.actions.new'),
empty: emptyLabel ?? t('shared.statuses.empty'), empty: t('shared.statuses.empty'),
}), }),
[t, modeLabels, addLabel, emptyLabel], [t],
) )
const values = useMemo( const values = useMemo(() => splitValue(value), [value])
() => splitValue(value, splitPattern),
[value, splitPattern],
)
const items = useMemo(() => { const items = useMemo(() => {
const counts = new Map<string, number>() const counts = new Map<string, number>()
@@ -92,7 +75,7 @@ export const BaseSplitChipEditor = ({
}, [values]) }, [values])
const handleAddDraft = () => { const handleAddDraft = () => {
const nextValues = splitValue(draft, splitPattern) const nextValues = splitValue(draft)
if (!nextValues.length) { if (!nextValues.length) {
return return
} }
@@ -113,7 +96,7 @@ export const BaseSplitChipEditor = ({
const resolvedAriaLabel = const resolvedAriaLabel =
ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined) ariaLabel ?? (typeof toggleLabel === 'string' ? toggleLabel : undefined)
const modeToggle = showModeToggle ? ( const modeToggle = (
<Tooltip title={toggleLabel}> <Tooltip title={toggleLabel}>
<IconButton <IconButton
size="small" size="small"
@@ -128,7 +111,7 @@ export const BaseSplitChipEditor = ({
<ToggleIcon fontSize="small" /> <ToggleIcon fontSize="small" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
) : null )
return ( return (
<> <>
@@ -202,7 +185,7 @@ export const BaseSplitChipEditor = ({
disabled={disabled} disabled={disabled}
size="small" size="small"
multiline multiline
rows={rows} rows={4}
sx={{ width: '100%' }} sx={{ width: '100%' }}
value={value} value={value}
helperText={helperText} helperText={helperText}
@@ -464,21 +464,6 @@ export const ConnectionTable = (props: Props) => {
] ]
}, [t]) }, [t])
useEffect(() => {
setColumnOrder((prevValue) => {
const baseFields = baseColumns.map((col) => col.field)
const prev = Array.isArray(prevValue) ? prevValue : []
const reconciled = reconcileColumnOrder(prev, baseFields)
if (
reconciled.length === prev.length &&
reconciled.every((field, i) => field === prev[i])
) {
return prevValue
}
return reconciled
})
}, [baseColumns, setColumnOrder])
const orderedColumns = useMemo(() => { const orderedColumns = useMemo(() => {
const baseFields = baseColumns.map((column) => column.field) const baseFields = baseColumns.map((column) => column.field)
const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields) const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields)
+67 -82
View File
@@ -1,4 +1,3 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { import {
AccessTimeRounded, AccessTimeRounded,
ArrowDropDown, ArrowDropDown,
@@ -31,7 +30,15 @@ import {
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import {
useCallback,
useEffect,
useEffectEvent,
useId,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
@@ -570,20 +577,22 @@ export const CurrentProxyCard = () => {
return profileValue return profileValue
} }
if (profileKey !== baseKey) { return profileKey !== baseKey ? localStorage.getItem(baseKey) : null
const legacyValue = localStorage.getItem(baseKey)
if (legacyValue != null) {
localStorage.removeItem(baseKey)
localStorage.setItem(profileKey, legacyValue)
return legacyValue
}
}
return null
}, },
[getProfileStorageKey], [getProfileStorageKey],
) )
// Hand the pre-profile-scoping group key to the first profile that shows the card.
useEffect(() => {
const profileKey = getProfileStorageKey(STORAGE_KEY_GROUP)
if (profileKey === STORAGE_KEY_GROUP) return
if (localStorage.getItem(profileKey) != null) return
const legacyValue = localStorage.getItem(STORAGE_KEY_GROUP)
if (legacyValue == null) return
localStorage.removeItem(STORAGE_KEY_GROUP)
localStorage.setItem(profileKey, legacyValue)
}, [getProfileStorageKey])
const writeProfileScopedItem = useCallback( const writeProfileScopedItem = useCallback(
(baseKey: string, value: string) => { (baseKey: string, value: string) => {
if (typeof window === 'undefined') return if (typeof window === 'undefined') return
@@ -596,7 +605,7 @@ export const CurrentProxyCard = () => {
[getProfileStorageKey], [getProfileStorageKey],
) )
const { handleSelectChange } = useProxySelection({ const { select } = useProxySelection({
onSuccess: () => { onSuccess: () => {
refreshProxy() refreshProxy()
}, },
@@ -615,25 +624,17 @@ export const CurrentProxyCard = () => {
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0 return savedSortType ? (Number(savedSortType) as ProxySortType) : 0
}) })
const [selectedGroupName, setSelectedGroupName] = useState('') const [lastGroupName, setLastGroupName] = useState('')
const [openSelect, setOpenSelect] = useState<OpenSelect>(null) const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
const [openSelectMode, setOpenSelectMode] = useState(mode)
useEffect(() => { if (openSelectMode !== mode) {
setOpenSelectMode(mode)
setOpenSelect(null) setOpenSelect(null)
}, [mode]) }
const delayButtonRef = useRef<HTMLButtonElement>(null) const delayButtonRef = useRef<HTMLButtonElement>(null)
const delays = useGroupDelays(selectedGroupName || null)
const autoCheckInProgressRef = useRef(false) const autoCheckInProgressRef = useRef(false)
const latestTimeoutRef = useRef<number>(
verge?.default_latency_timeout || 10000,
)
const latestProxyMemberRef = useRef<ResolvedProxyMember | null>(null)
useEffect(() => {
latestTimeoutRef.current = verge?.default_latency_timeout || 10000
}, [verge?.default_latency_timeout])
const selectableGroups = useMemo(() => { const selectableGroups = useMemo(() => {
if (!proxyView) return [] if (!proxyView) return []
@@ -644,6 +645,39 @@ export const CurrentProxyCard = () => {
) )
}, [proxyView]) }, [proxyView])
const selectedGroupName = useMemo(() => {
if (isDirectMode) return 'DIRECT'
if (!proxyView) return lastGroupName
if (isGlobalMode) return proxyView.global?.name ?? 'GLOBAL'
const hasGroup = (name: string | null) =>
!!name && selectableGroups.some((group) => group.name === name)
if (hasGroup(lastGroupName)) return lastGroupName
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
if (hasGroup(savedGroup)) return savedGroup!
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
const primaryGroup =
selectableGroups.find((group) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ?? selectableGroups[0]
return primaryGroup?.name ?? ''
}, [
isDirectMode,
isGlobalMode,
lastGroupName,
proxyView,
readProfileScopedItem,
selectableGroups,
])
// Keep the shown group, auto picks included, until it leaves the profile.
if (proxyView && lastGroupName !== selectedGroupName) {
setLastGroupName(selectedGroupName)
}
const delays = useGroupDelays(selectedGroupName || null)
const selectedGroup = useMemo<ProxyGroupView | null>(() => { const selectedGroup = useMemo<ProxyGroupView | null>(() => {
if (!proxyView || isDirectMode) return null if (!proxyView || isDirectMode) return null
if (isGlobalMode) return proxyView.global if (isGlobalMode) return proxyView.global
@@ -680,46 +714,6 @@ export const CurrentProxyCard = () => {
[optionsForGroup, selectedGroup], [optionsForGroup, selectedGroup],
) )
useEffect(() => {
if (!proxyView) return
if (isDirectMode) {
setSelectedGroupName('DIRECT')
return
}
if (isGlobalMode) {
setSelectedGroupName(proxyView.global?.name ?? 'GLOBAL')
return
}
const savedGroup = readProfileScopedItem(STORAGE_KEY_GROUP)
const primaryKeywords = ['auto', 'select', 'proxy', '节点选择', '自动选择']
const primaryGroup =
selectableGroups.find((group) =>
primaryKeywords.some((keyword) =>
group.name.toLowerCase().includes(keyword.toLowerCase()),
),
) ?? selectableGroups[0]
const nextGroup = selectableGroups.some(
({ name }) => name === selectedGroupName,
)
? selectedGroupName
: selectableGroups.some(({ name }) => name === savedGroup)
? savedGroup!
: (primaryGroup?.name ?? '')
if (nextGroup !== selectedGroupName) {
setSelectedGroupName(nextGroup)
if (nextGroup) writeProfileScopedItem(STORAGE_KEY_GROUP, nextGroup)
}
}, [
isDirectMode,
isGlobalMode,
proxyView,
readProfileScopedItem,
selectableGroups,
selectedGroupName,
writeProfileScopedItem,
])
const currentOption = useMemo(() => { const currentOption = useMemo(() => {
if (!proxyView) return undefined if (!proxyView) return undefined
if (isDirectMode) { if (isDirectMode) {
@@ -743,12 +737,10 @@ export const CurrentProxyCard = () => {
: undefined : undefined
}, [isDirectMode, proxyView, selectedGroup]) }, [isDirectMode, proxyView, selectedGroup])
latestProxyMemberRef.current = currentOption?.member ?? null
const handleGroupChange = useCallback( const handleGroupChange = useCallback(
(newGroupName: string) => { (newGroupName: string) => {
if (isGlobalMode || isDirectMode) return if (isGlobalMode || isDirectMode) return
setSelectedGroupName(newGroupName) setLastGroupName(newGroupName)
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName) writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
}, },
[isDirectMode, isGlobalMode, writeProfileScopedItem], [isDirectMode, isGlobalMode, writeProfileScopedItem],
@@ -766,15 +758,9 @@ export const CurrentProxyCard = () => {
const previousProxy = selectedGroup.now const previousProxy = selectedGroup.now
const nextName = option.member.ref.name const nextName = option.member.ref.name
// The profile selection is the durable source across core restarts and run modes. // The profile selection is the durable source across core restarts and run modes.
handleSelectChange( select(selectedGroup.name, nextName, previousProxy!, selectedGroup.fixed!)
selectedGroup.name,
previousProxy!,
selectedGroup.fixed!,
)({
target: { value: nextName },
})
}, },
[handleSelectChange, isDirectMode, selectedGroup, unsortedProxyOptions], [isDirectMode, select, selectedGroup, unsortedProxyOptions],
) )
const goToProxies = useCallback(() => { const goToProxies = useCallback(() => {
@@ -799,7 +785,7 @@ export const CurrentProxyCard = () => {
color: 'text.secondary', color: 'text.secondary',
} }
const checkCurrentProxyDelay = useCallback(async () => { const checkCurrentProxyDelay = useEffectEvent(async () => {
if (autoCheckInProgressRef.current) return if (autoCheckInProgressRef.current) return
if (isDirectMode) return if (isDirectMode) return
@@ -808,7 +794,7 @@ export const CurrentProxyCard = () => {
if (!groupName || !proxyName) return if (!groupName || !proxyName) return
const proxyMember = latestProxyMemberRef.current const proxyMember = currentOption?.member
if (!proxyMember || !isInteractableMember(proxyMember)) { if (!proxyMember || !isInteractableMember(proxyMember)) {
debugLog( debugLog(
`[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`, `[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`,
@@ -818,7 +804,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = true autoCheckInProgressRef.current = true
const timeout = latestTimeoutRef.current || 10000 const timeout = verge?.default_latency_timeout || 10000
try { try {
debugLog( debugLog(
@@ -834,7 +820,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = false autoCheckInProgressRef.current = false
refreshProxy() refreshProxy()
} }
}, [isDirectMode, refreshProxy, selectedGroupName, selectedProxyName]) })
useEffect(() => { useEffect(() => {
if (isDirectMode) return if (isDirectMode) return
@@ -864,7 +850,6 @@ export const CurrentProxyCard = () => {
if (intervalTimer) clearTimeout(intervalTimer) if (intervalTimer) clearTimeout(intervalTimer)
} }
}, [ }, [
checkCurrentProxyDelay,
autoDelayIntervalMs, autoDelayIntervalMs,
isDirectMode, isDirectMode,
selectedGroupName, selectedGroupName,
@@ -4,6 +4,7 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useEffectEvent,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useReducer, useReducer,
@@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next'
import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor' import { useTrafficGraphDataEnhanced } from '@/hooks/use-traffic-monitor'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { debugLog } from '@/utils/debug'
import parseTraffic from '@/utils/parse-traffic' import parseTraffic from '@/utils/parse-traffic'
import { import {
formatTrafficHourMinute, formatTrafficHourMinute,
@@ -23,14 +23,7 @@ import {
isSameTrafficData, isSameTrafficData,
} from '@/utils/traffic-sampler' } from '@/utils/traffic-sampler'
interface ITrafficItem {
up: number
down: number
timestamp?: number
}
export interface EnhancedCanvasTrafficGraphRef { export interface EnhancedCanvasTrafficGraphRef {
appendData: (data: ITrafficItem) => void
toggleStyle: () => void toggleStyle: () => void
} }
@@ -128,7 +121,6 @@ export const EnhancedCanvasTrafficGraph = memo(
const drawFrameRef = useRef<number | undefined>(undefined) const drawFrameRef = useRef<number | undefined>(undefined)
const hoverFrameRef = useRef<number | undefined>(undefined) const hoverFrameRef = useRef<number | undefined>(undefined)
const mouseMoveFrameRef = useRef<number | undefined>(undefined) const mouseMoveFrameRef = useRef<number | undefined>(undefined)
const scheduleDrawGraphRef = useRef<() => void>(() => {})
const pendingMousePositionRef = useRef<{ const pendingMousePositionRef = useRef<{
clientX: number clientX: number
clientY: number clientY: number
@@ -142,7 +134,6 @@ export const EnhancedCanvasTrafficGraph = memo(
[], [],
) )
const debounceTimeoutRef = useRef<number | null>(null) const debounceTimeoutRef = useRef<number | null>(null)
const [currentFPS, setCurrentFPS] = useState(GRAPH_CONFIG.targetFPS)
const colors = useMemo( const colors = useMemo(
() => ({ () => ({
@@ -183,8 +174,6 @@ export const EnhancedCanvasTrafficGraph = memo(
if (displayData.length === 0) { if (displayData.length === 0) {
lastDataTimestampRef.current = 0 lastDataTimestampRef.current = 0
dataStaleRef.current = false dataStaleRef.current = false
// eslint-disable-next-line @eslint-react/set-state-in-effect
setCurrentFPS(GRAPH_CONFIG.targetFPS)
return return
} }
@@ -200,18 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
} }
}, [displayData]) }, [displayData])
const handleFocusStateChange = useCallback( const handleFocusStateChange = useEffectEvent((focused: boolean) => {
(focused: boolean) => { isWindowFocusedRef.current = focused
isWindowFocusedRef.current = focused
if (focused || !pause_render_traffic_stats_on_blur) { scheduleDrawGraph()
setCurrentFPS(GRAPH_CONFIG.targetFPS) })
}
scheduleDrawGraphRef.current()
},
[pause_render_traffic_stats_on_blur],
)
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') { if (typeof window === 'undefined' || typeof document === 'undefined') {
@@ -235,7 +217,7 @@ export const EnhancedCanvasTrafficGraph = memo(
window.removeEventListener('blur', handleBlur) window.removeEventListener('blur', handleBlur)
document.removeEventListener('visibilitychange', handleVisibilityChange) document.removeEventListener('visibilitychange', handleVisibilityChange)
} }
}, [handleFocusStateChange]) }, [])
const calculateY = useCallback( const calculateY = useCallback(
( (
@@ -684,19 +666,13 @@ export const EnhancedCanvasTrafficGraph = memo(
ctx.beginPath() ctx.beginPath()
ctx.moveTo(getX(0), getY(0)) ctx.moveTo(getX(0), getY(0))
if (chartStyle === 'bezier') { for (let i = 1; i < data.length; i++) {
for (let i = 1; i < data.length; i++) { const currentX = getX(i)
const currentX = getX(i) const currentY = getY(i)
const currentY = getY(i) const nextIndex = Math.min(i + 1, lastIndex)
const nextIndex = Math.min(i + 1, lastIndex) const controlX = (currentX + getX(nextIndex)) / 2
const controlX = (currentX + getX(nextIndex)) / 2 const controlY = (currentY + getY(nextIndex)) / 2
const controlY = (currentY + getY(nextIndex)) / 2 ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
ctx.quadraticCurveTo(currentX, currentY, controlX, controlY)
}
} else {
for (let i = 1; i < data.length; i++) {
ctx.lineTo(getX(i), getY(i))
}
} }
ctx.lineTo(getX(lastIndex), height - padding.bottom) ctx.lineTo(getX(lastIndex), height - padding.bottom)
@@ -930,25 +906,21 @@ export const EnhancedCanvasTrafficGraph = memo(
scheduleDrawGraph() scheduleDrawGraph()
}, [scheduleDrawGraph]) }, [scheduleDrawGraph])
useEffect(() => { const redrawGraph = useEffectEvent(() => scheduleDrawGraph())
scheduleDrawGraphRef.current = scheduleDrawGraph
}, [scheduleDrawGraph])
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current const canvas = canvasRef.current
if (!canvas || typeof window === 'undefined') return if (!canvas || typeof window === 'undefined') return
if (typeof ResizeObserver === 'undefined') { if (typeof ResizeObserver === 'undefined') {
const handleResize = () => scheduleDrawGraphRef.current() const handleResize = () => redrawGraph()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return () => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
} }
const resizeObserver = new ResizeObserver(() => const resizeObserver = new ResizeObserver(() => redrawGraph())
scheduleDrawGraphRef.current(),
)
resizeObserver.observe(canvas) resizeObserver.observe(canvas)
return () => { return () => {
@@ -984,21 +956,7 @@ export const EnhancedCanvasTrafficGraph = memo(
setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier')) setChartStyle((prev) => (prev === 'bezier' ? 'line' : 'bezier'))
}, []) }, [])
const appendData = useCallback((data: ITrafficItem) => { useImperativeHandle(ref, () => ({ toggleStyle }), [toggleStyle])
debugLog(
'[EnhancedCanvasTrafficGraphV2] appendData called (using global data):',
data,
)
}, [])
useImperativeHandle(
ref,
() => ({
appendData,
toggleStyle,
}),
[appendData, toggleStyle],
)
const getTimeRangeText = useCallback(() => { const getTimeRangeText = useCallback(() => {
return t('home.components.traffic.patterns.minutes', { return t('home.components.traffic.patterns.minutes', {
@@ -1142,7 +1100,7 @@ export const EnhancedCanvasTrafficGraph = memo(
{t('home.components.traffic.diagnostics', { {t('home.components.traffic.diagnostics', {
points: displayData.length, points: displayData.length,
compressed: samplerStats.compressedBufferSize, compressed: samplerStats.compressedBufferSize,
fps: currentFPS, fps: GRAPH_CONFIG.targetFPS,
})} })}
</Box> </Box>
+55 -6
View File
@@ -3,10 +3,14 @@ import {
ArrowUpwardRounded, ArrowUpwardRounded,
CloudDownloadRounded, CloudDownloadRounded,
CloudUploadRounded, CloudUploadRounded,
ErrorOutlineRounded,
LinkRounded, LinkRounded,
MemoryRounded, MemoryRounded,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
Alert,
Box,
Button,
Grid, Grid,
PaletteColor, PaletteColor,
Paper, Paper,
@@ -15,9 +19,9 @@ import {
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import { ReactNode, memo, useMemo, useRef } from 'react' import { ReactNode, memo, useMemo, useRef } from 'react'
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
import { useConnectionSummaryData } from '@/hooks/use-connection-data' import { useConnectionSummaryData } from '@/hooks/use-connection-data'
import { useMemoryData } from '@/hooks/use-memory-data' import { useMemoryData } from '@/hooks/use-memory-data'
import { useTrafficData } from '@/hooks/use-traffic-data' import { useTrafficData } from '@/hooks/use-traffic-data'
@@ -127,6 +131,50 @@ const CompactStatCard = memo(
// 添加显示名称 // 添加显示名称
CompactStatCard.displayName = 'CompactStatCard' CompactStatCard.displayName = 'CompactStatCard'
const fallbackSx = {
p: 2,
minHeight: 200,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed',
borderColor: 'error.main',
borderRadius: 2,
bgcolor: 'error.light',
}
const reloadPage = () => window.location.reload()
const TrafficFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
const { t } = useTranslation()
return (
<Box sx={fallbackSx}>
<ErrorOutlineRounded color="error" sx={{ fontSize: 48, mb: 2 }} />
<Typography variant="h6" gutterBottom>
{t('shared.feedback.errors.trafficStats')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t('shared.feedback.errors.trafficStatsDescription')}
</Typography>
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
{error instanceof Error
? error.message
: t('shared.feedback.errors.unknown')}
</Alert>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button variant="contained" size="small" onClick={resetErrorBoundary}>
{t('shared.actions.retry')}
</Button>
<Button variant="outlined" size="small" onClick={reloadPage}>
{t('shared.actions.refreshPage')}
</Button>
</Box>
</Box>
)
}
export const EnhancedTrafficStats = () => { export const EnhancedTrafficStats = () => {
const { t } = useTranslation() const { t } = useTranslation()
const theme = useTheme() const theme = useTheme()
@@ -254,10 +302,11 @@ export const EnhancedTrafficStats = () => {
}, [t, parsedData, displayMemory]) }, [t, parsedData, displayMemory])
return ( return (
<TrafficErrorBoundary <ErrorBoundary
onError={(error, errorInfo) => { FallbackComponent={TrafficFallback}
console.error('[EnhancedTrafficStats] 组件错误:', error, errorInfo) onError={(error, errorInfo) =>
}} console.error('[EnhancedTrafficStats]', error, errorInfo)
}
> >
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}> <Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
{trafficGraph && ( {trafficGraph && (
@@ -273,6 +322,6 @@ export const EnhancedTrafficStats = () => {
</Grid> </Grid>
))} ))}
</Grid> </Grid>
</TrafficErrorBoundary> </ErrorBoundary>
) )
} }
+12 -7
View File
@@ -14,7 +14,7 @@ import {
useTheme, useTheme,
Fade, Fade,
} from '@mui/material' } from '@mui/material'
import { useState, useMemo, memo, FC } from 'react' import { useState, useMemo, useCallback, memo, FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ProxyControlSwitches from '@/components/shared/proxy-control-switches' import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
@@ -152,10 +152,15 @@ export const ProxyTunCard: FC = () => {
showNotice.error(err) showNotice.error(err)
} }
const handleTabChange = (tab: string) => { const handleSystemTabClick = useCallback(() => {
setActiveTab(tab) setActiveTab('system')
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab) localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'system')
} }, [])
const handleTunTabClick = useCallback(() => {
setActiveTab('tun')
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, 'tun')
}, [])
const tabDescription = useMemo(() => { const tabDescription = useMemo(() => {
if (activeTab === 'system') { if (activeTab === 'system') {
@@ -191,14 +196,14 @@ export const ProxyTunCard: FC = () => {
> >
<TabButton <TabButton
isActive={activeTab === 'system'} isActive={activeTab === 'system'}
onClick={() => handleTabChange('system')} onClick={handleSystemTabClick}
icon={ComputerRounded} icon={ComputerRounded}
label={t('settings.sections.system.toggles.systemProxy')} label={t('settings.sections.system.toggles.systemProxy')}
hasIndicator={systemProxyIndicator} hasIndicator={systemProxyIndicator}
/> />
<TabButton <TabButton
isActive={activeTab === 'tun'} isActive={activeTab === 'tun'}
onClick={() => handleTabChange('tun')} onClick={handleTunTabClick}
icon={TroubleshootRounded} icon={TroubleshootRounded}
label={t('settings.sections.system.toggles.tunMode')} label={t('settings.sections.system.toggles.tunMode')}
hasIndicator={enable_tun_mode! && isTunModeAvailable} hasIndicator={enable_tun_mode! && isTunModeAvailable}
+24 -33
View File
@@ -8,7 +8,7 @@ import {
} from '@mui/icons-material' } from '@mui/icons-material'
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material' import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
@@ -22,10 +22,23 @@ import {
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { getSystemInfo } from '@/services/cmds' import { getSystemInfo } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { version as appVersion } from '@root/package.json' import { version as appVersion } from '@root/package.json'
import { EnhancedCard } from './enhanced-card' import { EnhancedCard } from './enhanced-card'
const getOsInfo = async () => {
const info = await getSystemInfo()
const sysName = info.system_name
let sysVersion = info.system_version
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
sysVersion = sysVersion.substring(sysName.length).trim()
}
return `${sysName} ${sysVersion}`
}
export const SystemInfoCard = () => { export const SystemInfoCard = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
@@ -36,31 +49,16 @@ export const SystemInfoCard = () => {
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true) const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
const [osInfo, setOsInfo] = useState('') const { data: osInfo = '' } = useQuery({
queryKey: ['getSystemInfo'],
queryFn: getOsInfo,
})
const lastCheckUpdateText = useMemo( const lastCheckUpdateText = useMemo(
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'), () => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
[lastCheckUpdate], [lastCheckUpdate],
) )
useEffect(() => {
getSystemInfo()
.then((info) => {
const sysName = info.system_name
let sysVersion = info.system_version
if (
sysName &&
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
) {
sysVersion = sysVersion.substring(sysName.length).trim()
}
setOsInfo(`${sysName} ${sysVersion}`)
})
.catch(console.error)
}, [])
useEffect(() => { useEffect(() => {
if (!verge?.auto_check_update) return if (!verge?.auto_check_update) return
if (readLastCheckTime() !== null) return if (readLastCheckTime() !== null) return
@@ -86,16 +84,11 @@ export const SystemInfoCard = () => {
}, [verge, patchVerge]) }, [verge, patchVerge])
const handleRunningModeClick = useCallback(async () => { const handleRunningModeClick = useCallback(async () => {
if (isSidecarMode || (isAdminMode && isSidecarMode)) { if (isSidecarMode) {
await installServiceAndRestartCore() await installServiceAndRestartCore()
await mutateSystemState() await mutateSystemState()
} }
}, [ }, [isSidecarMode, installServiceAndRestartCore, mutateSystemState])
isSidecarMode,
isAdminMode,
installServiceAndRestartCore,
mutateSystemState,
])
const onCheckUpdate = useLockFn(async () => { const onCheckUpdate = useLockFn(async () => {
try { try {
@@ -122,18 +115,16 @@ export const SystemInfoCard = () => {
const runningModeStyle = useMemo( const runningModeStyle = useMemo(
() => ({ () => ({
cursor: cursor: isSidecarMode ? 'pointer' : 'default',
isSidecarMode || (isAdminMode && isSidecarMode) ? 'pointer' : 'default', textDecoration: isSidecarMode ? 'underline' : 'none',
textDecoration:
isSidecarMode || (isAdminMode && isSidecarMode) ? 'underline' : 'none',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 0.5, gap: 0.5,
'&:hover': { '&:hover': {
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1, opacity: isSidecarMode ? 0.7 : 1,
}, },
}), }),
[isSidecarMode, isAdminMode], [isSidecarMode],
) )
const getModeIcon = () => { const getModeIcon = () => {
+3 -3
View File
@@ -87,7 +87,7 @@ export const TestCard = () => {
x.uid === uid ? { ...x, ...patch } : x, x.uid === uid ? { ...x, ...patch } : x,
) )
mutateVerge({ ...verge, test_list: newList }, false) mutateVerge({ ...verge, test_list: newList })
}, },
[testList, verge, mutateVerge], [testList, verge, mutateVerge],
) )
@@ -96,7 +96,7 @@ export const TestCard = () => {
(uid: string) => { (uid: string) => {
const newList = testList.filter((x) => x.uid !== uid) const newList = testList.filter((x) => x.uid !== uid)
patchVerge({ test_list: newList }) patchVerge({ test_list: newList })
mutateVerge({ ...verge, test_list: newList }, false) mutateVerge({ ...verge, test_list: newList })
}, },
[testList, verge, patchVerge, mutateVerge], [testList, verge, patchVerge, mutateVerge],
) )
@@ -129,7 +129,7 @@ export const TestCard = () => {
newList.splice(newIndex, 0, removed) newList.splice(newIndex, 0, removed)
// 优化:先本地更新,再异步 patch,避免UI卡死 // 优化:先本地更新,再异步 patch,避免UI卡死
mutateVerge({ ...verge, test_list: newList }, false) mutateVerge({ ...verge, test_list: newList })
const patchFn = () => { const patchFn = () => {
try { try {
patchVerge({ test_list: newList }) patchVerge({ test_list: newList })
+1 -4
View File
@@ -35,10 +35,7 @@ export const LayoutSidebar = (props: LayoutSidebarProps) => {
const handleMenuOrderOptimisticUpdate = useCallback( const handleMenuOrderOptimisticUpdate = useCallback(
(order: string[]) => { (order: string[]) => {
mutateVerge( mutateVerge((prev) => (prev ? { ...prev, menu_order: order } : prev))
(prev) => (prev ? { ...prev, menu_order: order } : prev),
false,
)
}, },
[mutateVerge], [mutateVerge],
) )
+33 -4
View File
@@ -1,14 +1,15 @@
import { import {
ArrowDownwardRounded, ArrowDownwardRounded,
ArrowUpwardRounded, ArrowUpwardRounded,
ErrorOutlineRounded,
MemoryRounded, MemoryRounded,
} from '@mui/icons-material' } from '@mui/icons-material'
import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material' import type { BoxProps, SvgIconProps, TypographyProps } from '@mui/material'
import { Box, Typography } from '@mui/material' import { Box, Typography } from '@mui/material'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { LightweightTrafficErrorBoundary } from '@/components/shared/traffic-error-boundary'
import { useMemoryData } from '@/hooks/use-memory-data' import { useMemoryData } from '@/hooks/use-memory-data'
import { useTrafficData } from '@/hooks/use-traffic-data' import { useTrafficData } from '@/hooks/use-traffic-data'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
@@ -17,6 +18,30 @@ import parseTraffic from '@/utils/parse-traffic'
import { TrafficGraph, type TrafficRef } from './traffic-graph' import { TrafficGraph, type TrafficRef } from './traffic-graph'
const unavailableSx = {
p: 1,
minHeight: 60,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'error.light',
borderRadius: 1,
color: 'error.contrastText',
}
const TrafficUnavailableFallback = () => {
const { t } = useTranslation()
return (
<Box sx={unavailableSx}>
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="caption">
{t('shared.feedback.errors.trafficUnavailable')}
</Typography>
</Box>
)
}
export const LayoutTraffic = () => { export const LayoutTraffic = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { verge } = useVerge() const { verge } = useVerge()
@@ -75,7 +100,12 @@ export const LayoutTraffic = () => {
} }
return ( return (
<LightweightTrafficErrorBoundary> <ErrorBoundary
FallbackComponent={TrafficUnavailableFallback}
onError={(error, errorInfo) =>
console.error('[LayoutTraffic]', error, errorInfo)
}
>
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
{trafficGraph && pageVisible && ( {trafficGraph && pageVisible && (
<div <div
@@ -130,7 +160,6 @@ export const LayoutTraffic = () => {
cursor: 'auto', cursor: 'auto',
}} }}
color={'disabled'} color={'disabled'}
onClick={async () => {}}
> >
<MemoryRounded {...iconStyle} /> <MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography> <Typography {...valStyle}>{inuse}</Typography>
@@ -139,6 +168,6 @@ export const LayoutTraffic = () => {
)} )}
</Box> </Box>
</Box> </Box>
</LightweightTrafficErrorBoundary> </ErrorBoundary>
) )
} }
+4 -36
View File
@@ -1,11 +1,6 @@
import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material' import { Close, CropSquare, FilterNone, Minimize } from '@mui/icons-material'
import { Box, IconButton } from '@mui/material' import { Box, IconButton } from '@mui/material'
import { import { type PointerEvent, useCallback } from 'react'
forwardRef,
type PointerEvent,
useCallback,
useImperativeHandle,
} from 'react'
import { useWindowControls } from '@/hooks/use-window' import { useWindowControls } from '@/hooks/use-window'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
@@ -63,36 +58,9 @@ export const WindowResizeHandles = () => {
) )
} }
export const WindowControls = forwardRef(function WindowControls(props, ref) { export const WindowControls = () => {
const OS = getSystem() const OS = getSystem()
const { const { maximized, minimize, close, toggleMaximize } = useWindowControls()
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
} = useWindowControls()
useImperativeHandle(
ref,
() => ({
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
}),
[
currentWindow,
maximized,
minimize,
close,
toggleFullscreen,
toggleMaximize,
],
)
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。 // 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。 // 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
@@ -186,4 +154,4 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
)} )}
</Box> </Box>
) )
}) }
+5 -7
View File
@@ -21,6 +21,7 @@ import {
memo, memo,
useCallback, useCallback,
useEffect, useEffect,
useEffectEvent,
useReducer, useReducer,
useRef, useRef,
useState, useState,
@@ -113,7 +114,6 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const setProfileLoading = useSetProfileLoading() const setProfileLoading = useSetProfileLoading()
const [showNextUpdate, setShowNextUpdate] = useState(false) const [showNextUpdate, setShowNextUpdate] = useState(false)
const showNextUpdateRef = useRef(false)
const [nextUpdateTime, setNextUpdateTime] = useState('') const [nextUpdateTime, setNextUpdateTime] = useState('')
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>( const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined, undefined,
@@ -203,10 +203,6 @@ const ProfileItemBase = (props: ProfileItemProps) => {
setShowNextUpdate(!showNextUpdate) setShowNextUpdate(!showNextUpdate)
} }
useEffect(() => {
showNextUpdateRef.current = showNextUpdate
}, [showNextUpdate])
useEffect(() => { useEffect(() => {
if (showNextUpdate) { if (showNextUpdate) {
fetchNextUpdateTime() fetchNextUpdateTime()
@@ -218,8 +214,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
updated, updated,
]) ])
const isNextUpdateShown = useEffectEvent(() => showNextUpdate)
useEffect(() => { useEffect(() => {
if (timerUpdateRevision === 0 || !showNextUpdateRef.current) return if (timerUpdateRevision === 0 || !isNextUpdateShown()) return
if (refreshTimeoutRef.current !== undefined) { if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current)
@@ -236,7 +234,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
}, [fetchNextUpdateTime, timerUpdateRevision]) }, [fetchNextUpdateTime, timerUpdateRevision])
useEffect(() => { useEffect(() => {
if (completedUpdateRevision === 0 || !showNextUpdateRef.current) return if (completedUpdateRevision === 0 || !isNextUpdateShown()) return
fetchNextUpdateTime() fetchNextUpdateTime()
}, [completedUpdateRevision, fetchNextUpdateTime]) }, [completedUpdateRevision, fetchNextUpdateTime])
+19 -14
View File
@@ -10,7 +10,7 @@ import {
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { useEffect, useImperativeHandle, useRef, useState } from 'react' import { useImperativeHandle, useRef, useState } from 'react'
import { Controller, useForm } from 'react-hook-form' import { Controller, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -77,17 +77,6 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
}, },
})) }))
const selfProxy = watch('option.self_proxy')
const withProxy = watch('option.with_proxy')
useEffect(() => {
if (selfProxy) setValue('option.with_proxy', false)
}, [selfProxy, setValue])
useEffect(() => {
if (withProxy) setValue('option.self_proxy', false)
}, [setValue, withProxy])
const handleOk = useLockFn( const handleOk = useLockFn(
handleSubmit(async (form) => { handleSubmit(async (form) => {
setLoading(true) setLoading(true)
@@ -419,7 +408,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel> <InputLabel>
{t('profiles.modals.profileForm.fields.useSystemProxy')} {t('profiles.modals.profileForm.fields.useSystemProxy')}
</InputLabel> </InputLabel>
<Switch checked={field.value!} {...field} color="primary" /> <Switch
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.self_proxy', false)
}}
color="primary"
/>
</StyledBox> </StyledBox>
)} )}
/> />
@@ -432,7 +429,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel> <InputLabel>
{t('profiles.modals.profileForm.fields.useClashProxy')} {t('profiles.modals.profileForm.fields.useClashProxy')}
</InputLabel> </InputLabel>
<Switch checked={field.value!} {...field} color="primary" /> <Switch
checked={field.value!}
{...field}
onChange={(_, checked) => {
field.onChange(checked)
if (checked) setValue('option.with_proxy', false)
}}
color="primary"
/>
</StyledBox> </StyledBox>
)} )}
/> />
+30
View File
@@ -1,3 +1,5 @@
import * as yaml from 'js-yaml'
import { import {
getRecord, getRecord,
rebindNode, rebindNode,
@@ -34,3 +36,31 @@ export const rebindProxyChainItems = (
delay: record?.history.at(-1)?.delay, delay: record?.history.at(-1)?.delay,
} }
}) })
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
export const parseChainConfigItems = (configData: string): ProxyChainItem[] => {
try {
// JSON is valid YAML, so one parser covers both persisted formats.
const parsedConfig = yaml.load(configData) as ParsedChainConfig | null
const timestamp = Date.now()
return (
parsedConfig?.proxies?.map((proxy, index) => ({
id: `${proxy.name}_${timestamp}_${index}`,
name: proxy.name,
type: proxy.type,
delay: undefined,
})) || []
)
} catch (error) {
console.error('Failed to process chain config data:', error)
return []
}
}
-43
View File
@@ -25,7 +25,6 @@ import {
keyframes, keyframes,
useTheme, useTheme,
} from '@mui/material' } from '@mui/material'
import * as yaml from 'js-yaml'
import { import {
type Ref, type Ref,
useCallback, useCallback,
@@ -60,18 +59,9 @@ const chainPointerSensor = PointerSensor.configure({
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown } type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
interface ProxyChainProps { interface ProxyChainProps {
proxyChain: ProxyChainItem[] proxyChain: ProxyChainItem[]
onUpdateChain: (chain: ProxyChainItem[]) => void onUpdateChain: (chain: ProxyChainItem[]) => void
chainConfigData?: string | null
onMarkUnsavedChanges?: () => void onMarkUnsavedChanges?: () => void
mode?: string mode?: string
selectedGroup?: string | null selectedGroup?: string | null
@@ -98,21 +88,6 @@ const arrowFadeIn = keyframes`
} }
` `
const toChainItems = (
parsedConfig: ParsedChainConfig | null | undefined,
): ProxyChainItem[] => {
const timestamp = Date.now()
return (
parsedConfig?.proxies?.map((proxy, index) => ({
id: `${proxy.name}_${timestamp}_${index}`,
name: proxy.name,
type: proxy.type,
delay: undefined,
})) || []
)
}
interface ChainCardProps { interface ChainCardProps {
proxy: ProxyChainItem proxy: ProxyChainItem
index: number index: number
@@ -328,7 +303,6 @@ const SortableProxyChainItem = ({
export const ProxyChain = ({ export const ProxyChain = ({
proxyChain, proxyChain,
onUpdateChain, onUpdateChain,
chainConfigData,
onMarkUnsavedChanges, onMarkUnsavedChanges,
mode, mode,
selectedGroup, selectedGroup,
@@ -580,23 +554,6 @@ export const ProxyChain = ({
recordSelection, recordSelection,
]) ])
// 处理链式代理配置数据
useEffect(() => {
if (chainConfigData) {
try {
// JSON is valid YAML, so one parser covers both persisted formats.
const parsedConfig = yaml.load(chainConfigData) as ParsedChainConfig
const chainItems = toChainItems(parsedConfig)
if (chainItems.length > 0) {
onUpdateChain(chainItems)
}
} catch (error) {
console.error('Failed to process chain config data:', error)
}
}
}, [chainConfigData, onUpdateChain])
return ( return (
<Paper <Paper
elevation={1} elevation={1}
+12 -3
View File
@@ -76,7 +76,7 @@ interface GroupSelectMenuProps {
interface ProxyGroupsChainProps { interface ProxyGroupsChainProps {
mode: string mode: string
chainConfigData?: string | null chainConfigItems?: ProxyChainItem[] | null
onExitChainMode: () => void onExitChainMode: () => void
availableGroups: any[] availableGroups: any[]
activeSelectedGroup: string | null activeSelectedGroup: string | null
@@ -305,7 +305,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
mode, mode,
chainConfigData, chainConfigItems,
onExitChainMode, onExitChainMode,
availableGroups, availableGroups,
activeSelectedGroup, activeSelectedGroup,
@@ -338,6 +338,16 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
return [] return []
}) })
// Each fetched runtime chain replaces the stored one once.
const configItems = chainConfigItems ?? null
const [appliedConfigItems, setAppliedConfigItems] = useState<
ProxyChainItem[] | null
>(null)
if (configItems !== appliedConfigItems) {
setAppliedConfigItems(configItems)
if (configItems?.length) setProxyChain(configItems)
}
const candidateNodes = useMemo( const candidateNodes = useMemo(
() => () =>
renderList.flatMap((item) => { renderList.flatMap((item) => {
@@ -520,7 +530,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
<ProxyChain <ProxyChain
proxyChain={currentProxyChain} proxyChain={currentProxyChain}
onUpdateChain={setProxyChain} onUpdateChain={setProxyChain}
chainConfigData={chainConfigData}
mode={mode} mode={mode}
selectedGroup={activeSelectedGroup} selectedGroup={activeSelectedGroup}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
+36 -10
View File
@@ -32,6 +32,7 @@ import {
} from '@/types/proxy-view' } from '@/types/proxy-view'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
import type { ProxyChainItem } from './proxy-chain-model'
import { ProxyEmptyState } from './proxy-empty-state' import { ProxyEmptyState } from './proxy-empty-state'
import { import {
resolveEmptyListReason, resolveEmptyListReason,
@@ -68,7 +69,7 @@ function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
interface Props { interface Props {
mode: string mode: string
isChainMode?: boolean isChainMode?: boolean
chainConfigData?: string | null chainConfigItems?: ProxyChainItem[] | null
onExitChainMode: () => void onExitChainMode: () => void
} }
@@ -183,6 +184,7 @@ function useProxyRenderState(
renderList, renderList,
onProxies, onProxies,
onHeadState, onHeadState,
setHeadState: setListHeadState,
handleCheckAll, handleCheckAll,
saveScrollPosition, saveScrollPosition,
getScrollPosition, getScrollPosition,
@@ -191,10 +193,10 @@ function useProxyRenderState(
function ChainProxyGroups(props: { function ChainProxyGroups(props: {
mode: string mode: string
chainConfigData?: string | null chainConfigItems?: ProxyChainItem[] | null
onExitChainMode: () => void onExitChainMode: () => void
}) { }) {
const { mode, chainConfigData, onExitChainMode } = props const { mode, chainConfigItems, onExitChainMode } = props
const { proxyView } = useProxiesData() const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null) const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
@@ -348,7 +350,7 @@ function ChainProxyGroups(props: {
<Suspense fallback={<BaseLoading />}> <Suspense fallback={<BaseLoading />}>
<ProxyGroupsChain <ProxyGroupsChain
mode={mode} mode={mode}
chainConfigData={chainConfigData} chainConfigItems={chainConfigItems}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
availableGroups={availableGroups} availableGroups={availableGroups}
activeSelectedGroup={activeSelectedGroup} activeSelectedGroup={activeSelectedGroup}
@@ -377,12 +379,17 @@ function NormalProxyGroups(props: { mode: string }) {
renderList, renderList,
onProxies, onProxies,
onHeadState, onHeadState,
setHeadState,
handleCheckAll, handleCheckAll,
getScrollPosition, getScrollPosition,
saveScrollPosition, saveScrollPosition,
} = useProxyRenderState(mode, false, null) } = useProxyRenderState(mode, false, null)
const emptyList = useEmptyRenderList() const emptyList = useEmptyRenderList()
const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout() const { onDragEnd: onHeaderDragEnd } = useProxyGroupHeaderLayout()
const { proxyView } = useProxiesData()
const globalGroupName = proxyView?.global?.name
// The revealed group sits below GLOBAL's rows, so a click must bring it into view.
const pendingRevealRef = useRef<string | null>(null)
const renderFirstRef = useRef(true) const renderFirstRef = useRef(true)
// Do not persist intermediate positions produced while restoring virtual scroll. // Do not persist intermediate positions produced while restoring virtual scroll.
const isRestoringRef = useRef(false) const isRestoringRef = useRef(false)
@@ -462,7 +469,7 @@ function NormalProxyGroups(props: { mode: string }) {
} }
}, [handleScroll]) }, [handleScroll])
const { handleProxyGroupChange } = useProxySelection({ const { select } = useProxySelection({
onSuccess: () => { onSuccess: () => {
onProxies() onProxies()
}, },
@@ -477,9 +484,17 @@ function NormalProxyGroups(props: { mode: string }) {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) return if (!isInteractableMember(member)) return
handleProxyGroupChange(group, { name: member.ref.name }) if (group.name === globalGroupName) {
pendingRevealRef.current =
member.kind === 'group' ? member.ref.name : null
// The selection refetches the view, so skip onHeadState's refetch.
if (member.kind === 'group') {
setHeadState(member.ref.name, { open: true })
}
}
select(group.name, member.ref.name, group.now!, group.fixed!)
}, },
[handleProxyGroupChange], [globalGroupName, select, setHeadState],
) )
const handleLocation = useStableCallback((group: ProxyGroupView) => { const handleLocation = useStableCallback((group: ProxyGroupView) => {
@@ -525,6 +540,17 @@ function NormalProxyGroups(props: { mode: string }) {
return Array.from(new Set(names)) return Array.from(new Set(names))
}, [renderList]) }, [renderList])
useEffect(() => {
if (mode !== 'global') {
pendingRevealRef.current = null
return
}
const groupName = pendingRevealRef.current
if (!groupName || !proxyGroupNames.includes(groupName)) return
pendingRevealRef.current = null
handleGroupLocationByName(groupName)
}, [handleGroupLocationByName, mode, proxyGroupNames])
const handleGroupToggle = useCallback( const handleGroupToggle = useCallback(
async (group: ProxyGroupView) => { async (group: ProxyGroupView) => {
const index = renderList.findIndex( const index = renderList.findIndex(
@@ -608,7 +634,7 @@ function NormalProxyGroups(props: { mode: string }) {
/> />
</DragDropProvider> </DragDropProvider>
{(mode === 'rule' || mode === 'global') && ( {mode === 'rule' && (
<ProxyGroupNavigator <ProxyGroupNavigator
proxyGroupNames={proxyGroupNames} proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName} onGroupLocation={handleGroupLocationByName}
@@ -621,7 +647,7 @@ function NormalProxyGroups(props: { mode: string }) {
} }
export const ProxyGroups = (props: Props) => { export const ProxyGroups = (props: Props) => {
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props const { mode, isChainMode = false, chainConfigItems, onExitChainMode } = props
const { profiles, isLoading: isProfilesLoading } = useProfiles() const { profiles, isLoading: isProfilesLoading } = useProfiles()
const { isProxyViewPending } = useProxiesData() const { isProxyViewPending } = useProxiesData()
const { isRunningModePending } = useSystemData() const { isRunningModePending } = useSystemData()
@@ -645,7 +671,7 @@ export const ProxyGroups = (props: Props) => {
return isChainMode ? ( return isChainMode ? (
<ChainProxyGroups <ChainProxyGroups
mode={mode} mode={mode}
chainConfigData={chainConfigData} chainConfigItems={chainConfigItems}
onExitChainMode={onExitChainMode} onExitChainMode={onExitChainMode}
/> />
) : ( ) : (
@@ -30,10 +30,8 @@ export const useProxyGroupHeaderLayout = () => {
if (canceled || !operation.source || !operation.target) return if (canceled || !operation.source || !operation.target) return
const position = toolsOnLeft ? 'right' : 'left' const position = toolsOnLeft ? 'right' : 'left'
mutateVerge( mutateVerge((prev) =>
(prev) => prev ? { ...prev, proxy_group_tools_position: position } : prev,
prev ? { ...prev, proxy_group_tools_position: position } : prev,
false,
) )
try { try {
await patchVerge({ proxy_group_tools_position: position }) await patchVerge({ proxy_group_tools_position: position })
+19 -18
View File
@@ -6,6 +6,7 @@ import { useVerge } from '@/hooks/use-verge'
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context' import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import delayManager, { type DelaySnapshot } from '@/services/delay' import delayManager, { type DelaySnapshot } from '@/services/delay'
import { import {
findCurrentGroupMember,
isInteractableMember, isInteractableMember,
resolveMember, resolveMember,
selectGlobalChainNodes, selectGlobalChainNodes,
@@ -209,8 +210,10 @@ export const useRenderList = (
const renderGroups = useMemo(() => { const renderGroups = useMemo(() => {
if (!proxyView) return [] if (!proxyView) return []
if (mode === 'rule') return proxyView.groups if (mode === 'rule') return proxyView.groups
const global = proxyView.global ? [proxyView.global] : [] const { global } = proxyView
return mode === 'global' ? [...global, ...proxyView.groups] : global if (!global) return []
const current = findCurrentGroupMember(proxyView, global)?.member
return current?.kind === 'group' ? [global, current.group] : [global]
}, [mode, proxyView]) }, [mode, proxyView])
// Every group this list draws, so a test settling in any of them re-sorts that group. // Every group this list draws, so a test settling in any of them re-sorts that group.
@@ -260,10 +263,10 @@ export const useRenderList = (
})) }))
} }
const useRule = mode === 'rule' || mode === 'global'
const cache = groupCacheRef.current const cache = groupCacheRef.current
const retList = renderGroups.flatMap((group) => { const retList = renderGroups.flatMap((group) => {
const isFlat = group === proxyView.global
const headState = headStates[group.name] || DEFAULT_STATE const headState = headStates[group.name] || DEFAULT_STATE
const cached = cache.get(group.name) const cached = cache.get(group.name)
if ( if (
@@ -279,17 +282,19 @@ export const useRenderList = (
} }
const ret: IRenderItem[] = [ const ret: IRenderItem[] = [
{ isFlat
type: 0, ? { type: 1, key: `head-${group.name}`, group, headState }
key: group.name, : {
group, type: 0,
headState, key: group.name,
icon: group.icon!, group,
testUrl: group.testUrl!, headState,
}, icon: group.icon!,
testUrl: group.testUrl!,
},
] ]
if (headState.open || !useRule) { if (headState.open || isFlat) {
const occurrences = filterSort( const occurrences = filterSort(
resolveOccurrences(proxyView, group), resolveOccurrences(proxyView, group),
group.name, group.name,
@@ -302,9 +307,6 @@ export const useRenderList = (
useRegularExpression: headState.filterUseRegularExpression, useRegularExpression: headState.filterUseRegularExpression,
}, },
) )
if (!useRule) {
ret.push({ type: 1, key: `head-${group.name}`, group, headState })
}
if (occurrences.length === 0) { if (occurrences.length === 0) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState }) ret.push({ type: 3, key: `empty-${group.name}`, group, headState })
} else if (col > 1) { } else if (col > 1) {
@@ -343,9 +345,8 @@ export const useRenderList = (
return ret return ret
}) })
const filtered = !useRule const filtered =
? retList.slice(1) mode === 'rule' ? retList.filter((item) => !item.group.hidden) : retList
: retList.filter((item) => !item.group.hidden)
if ( if (
prevListRef.current.length === filtered.length && prevListRef.current.length === filtered.length &&
filtered.every((item, index) => item === prevListRef.current[index]) filtered.every((item, index) => item === prevListRef.current[index])
@@ -114,17 +114,15 @@ export const BackupConfigViewer = memo(
errorNotice: false, errorNotice: false,
}, },
) )
await mutateVerge( await mutateVerge((current) =>
(current) => current
current ? {
? { ...current,
...current, webdav_url: trimmedUrl,
webdav_url: trimmedUrl, webdav_username: trimmedUsername,
webdav_username: trimmedUsername, webdav_password: data.password,
webdav_password: data.password, }
} : current,
: current,
false,
) )
setWebdavStatus(signature, 'unknown') setWebdavStatus(signature, 'unknown')
showNotice.success('settings.modals.backup.messages.webdavConfigSaved') showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
@@ -146,7 +146,12 @@ export const BackupHistoryViewer = ({
setRows([]) setRows([])
return return
} }
if (!isLocal && webdavStatus === 'failed' && !options?.force) { // Read the status here: as a dependency, the write below re-runs the fetch.
if (
!isLocal &&
getWebdavStatus(webdavSignature) === 'failed' &&
!options?.force
) {
setRows([]) setRows([])
return return
} }
@@ -180,7 +185,7 @@ export const BackupHistoryViewer = ({
setLoading(false) setLoading(false)
} }
}, },
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus], [buildRow, isLocal, open, shouldSkipWebDav, webdavSignature],
) )
useEffect(() => { useEffect(() => {
+126 -182
View File
@@ -190,6 +190,110 @@ const DEFAULT_DNS_CONFIG = {
}, },
} }
type DnsFormValues = {
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
fakeIpRange: string
fakeIpRange6: string
fakeIpFilterMode: 'blacklist' | 'whitelist'
preferH3: boolean
respectRules: boolean
useHosts: boolean
useSystemHosts: boolean
ipv6: boolean
fakeIpFilter: string
nameserver: string
fallback: string
defaultNameserver: string
proxyServerNameserver: string
directNameserver: string
directNameserverFollowPolicy: boolean
fallbackGeoip: boolean
fallbackGeoipCode: string
fallbackIpcidr: string
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置,独立于dns
}
const DEFAULT_FORM_VALUES: DnsFormValues = {
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
}
function buildDnsConfig(values: DnsFormValues) {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}
function buildDnsYaml(values: DnsFormValues) {
const config: Record<string, any> = {}
const dnsConfig = buildDnsConfig(values)
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
return yaml.dump(config, { forceQuotes: true })
}
export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) { export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation() const { t } = useTranslation()
const { mutateClash } = useClash() const { mutateClash } = useClash()
@@ -204,63 +308,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true) const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<MonacoEditorInstance | null>(null) const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<{ const [values, setValues] = useState<DnsFormValues>(DEFAULT_FORM_VALUES)
enable: boolean
listen: string
enhancedMode: 'fake-ip' | 'redir-host'
fakeIpRange: string
fakeIpRange6: string
fakeIpFilterMode: 'blacklist' | 'whitelist'
preferH3: boolean
respectRules: boolean
useHosts: boolean
useSystemHosts: boolean
ipv6: boolean
fakeIpFilter: string
nameserver: string
fallback: string
defaultNameserver: string
proxyServerNameserver: string
directNameserver: string
directNameserverFollowPolicy: boolean
fallbackGeoip: boolean
fallbackGeoipCode: string
fallbackIpcidr: string
fallbackDomain: string
nameserverPolicy: string
hosts: string // hosts设置,独立于dns
}>({
enable: DEFAULT_DNS_CONFIG.enable,
listen: DEFAULT_DNS_CONFIG.listen,
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver: DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
// Prevent YAML-to-form sync before configuration loads. // Prevent YAML-to-form sync before configuration loads.
const [yamlContent, setYamlContent] = useReducer( const [yamlContent, setYamlContent] = useReducer(
@@ -269,7 +318,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
) )
const updateValuesFromConfig = useCallback((config: any) => { const updateValuesFromConfig = useCallback((config: any) => {
if (!config) return if (!config) return undefined
const dnsConfig = config.dns || {} const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {} const hostsConfig = config.hosts || {}
@@ -289,7 +338,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? fakeIpFilterMode ? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'] : DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
setValues({ const next: DnsFormValues = {
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode, enhancedMode: validEnhancedMode,
@@ -341,127 +390,27 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
nameserverPolicy: nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '', formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
hosts: formatHosts(hostsConfig) || '', hosts: formatHosts(hostsConfig) || '',
}) }
setValues(next)
return next
}, []) }, [])
const generateDnsConfig = useCallback(() => {
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
'enhanced-mode': values.enhancedMode,
'fake-ip-range': values.fakeIpRange,
'fake-ip-range6': values.fakeIpRange6,
'fake-ip-filter-mode': values.fakeIpFilterMode,
'prefer-h3': values.preferH3,
'respect-rules': values.respectRules,
'use-hosts': values.useHosts,
'use-system-hosts': values.useSystemHosts,
ipv6: values.ipv6,
'fake-ip-filter': parseList(values.fakeIpFilter),
'default-nameserver': parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
'nameserver-policy': parseNameserverPolicy(values.nameserverPolicy),
'direct-nameserver-follow-policy': values.directNameserverFollowPolicy,
'fallback-filter': {
geoip: values.fallbackGeoip,
'geoip-code': values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
fallback: parseList(values.fallback),
'proxy-server-nameserver': parseList(values.proxyServerNameserver),
'direct-nameserver': parseList(values.directNameserver),
}
return dnsConfig
}, [values])
const updateYamlFromValues = useCallback(() => {
const config: Record<string, any> = {}
const dnsConfig = generateDnsConfig()
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
config.hosts = parseHosts(values.hosts)
setYamlContent(yaml.dump(config, { forceQuotes: true }))
}, [generateDnsConfig, values.hosts])
const resetToDefaults = useCallback(() => { const resetToDefaults = useCallback(() => {
setValues({ setValues(DEFAULT_FORM_VALUES)
enable: DEFAULT_DNS_CONFIG.enable, setYamlContent(buildDnsYaml(DEFAULT_FORM_VALUES))
listen: DEFAULT_DNS_CONFIG.listen, }, [])
enhancedMode: DEFAULT_DNS_CONFIG['enhanced-mode'],
fakeIpRange: DEFAULT_DNS_CONFIG['fake-ip-range'],
fakeIpRange6: DEFAULT_DNS_CONFIG['fake-ip-range6'],
fakeIpFilterMode: DEFAULT_DNS_CONFIG['fake-ip-filter-mode'],
preferH3: DEFAULT_DNS_CONFIG['prefer-h3'],
respectRules: DEFAULT_DNS_CONFIG['respect-rules'],
useHosts: DEFAULT_DNS_CONFIG['use-hosts'],
useSystemHosts: DEFAULT_DNS_CONFIG['use-system-hosts'],
ipv6: DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: DEFAULT_DNS_CONFIG['fake-ip-filter'].join(', '),
defaultNameserver: DEFAULT_DNS_CONFIG['default-nameserver'].join(', '),
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(', '),
fallback: DEFAULT_DNS_CONFIG.fallback.join(', '),
proxyServerNameserver:
DEFAULT_DNS_CONFIG['proxy-server-nameserver']?.join(', ') || '',
directNameserver:
DEFAULT_DNS_CONFIG['direct-nameserver']?.join(', ') || '',
directNameserverFollowPolicy:
DEFAULT_DNS_CONFIG['direct-nameserver-follow-policy'] || false,
fallbackGeoip: DEFAULT_DNS_CONFIG['fallback-filter'].geoip,
fallbackGeoipCode: DEFAULT_DNS_CONFIG['fallback-filter']['geoip-code'],
fallbackIpcidr:
DEFAULT_DNS_CONFIG['fallback-filter'].ipcidr?.join(', ') || '',
fallbackDomain:
DEFAULT_DNS_CONFIG['fallback-filter'].domain?.join(', ') || '',
nameserverPolicy: '',
hosts: '',
})
updateYamlFromValues()
}, [updateYamlFromValues])
const updateValuesFromYaml = useCallback(() => { const updateValuesFromYaml = useCallback(() => {
try { try {
const parsedYaml = yaml.load(yamlContent) as any const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml || parsedYaml.isInitValue) return if (!parsedYaml || parsedYaml.isInitValue) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml) updateValuesFromConfig(parsedYaml)
} catch { } catch {
showNotice.error('settings.modals.dns.errors.invalidYaml') showNotice.error('settings.modals.dns.errors.invalidYaml')
} }
}, [yamlContent, updateValuesFromConfig]) }, [yamlContent, updateValuesFromConfig])
useEffect(() => {
if (skipYamlSyncRef.current) {
skipYamlSyncRef.current = false
return
}
updateYamlFromValues()
}, [updateYamlFromValues])
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml)
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues)
useEffect(() => {
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml
latestUpdateYamlFromValuesRef.current = updateYamlFromValues
}, [updateValuesFromYaml, updateYamlFromValues])
useEffect(() => {
if (visualization) {
latestUpdateValuesFromYamlRef.current()
} else {
latestUpdateYamlFromValuesRef.current()
}
}, [visualization])
useEffect(() => { useEffect(() => {
return () => { return () => {
editorRef.current?.dispose() editorRef.current?.dispose()
@@ -476,8 +425,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (dnsConfig !== null) { if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any const config = yaml.load(dnsConfig) as any
updateValuesFromConfig(config) const loaded = updateValuesFromConfig(config)
setYamlContent(dnsConfig) setYamlContent(loaded ? buildDnsYaml(loaded) : dnsConfig)
} else { } else {
resetToDefaults() resetToDefaults()
} }
@@ -506,7 +455,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (visualization) { if (visualization) {
config = {} config = {}
const dnsConfig = generateDnsConfig() const dnsConfig = buildDnsConfig(values)
if (Object.keys(dnsConfig).length > 0) { if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig config.dns = dnsConfig
} }
@@ -586,20 +535,10 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? event.target.checked ? event.target.checked
: event.target.value : event.target.value
setValues((prev) => { setValues((prev) => ({
const newValues = { ...prev,
...prev, [field]: value,
[field]: value, }))
}
if (visualization) {
setTimeout(() => {
updateYamlFromValues()
}, 0)
}
return newValues
})
} }
return ( return (
@@ -629,7 +568,12 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="contained" variant="contained"
size="small" size="small"
onClick={() => { onClick={() => {
setVisualization((prev) => !prev) if (visualization) {
setYamlContent(buildDnsYaml(values))
} else {
updateValuesFromYaml()
}
setVisualization(!visualization)
}} }}
> >
{visualization {visualization
@@ -107,7 +107,7 @@ export const LayoutViewer = forwardRef<DialogRef>((_, ref) => {
showNotice.error(err) showNotice.error(err)
} }
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false) mutateVerge({ ...verge, ...patch })
} }
return ( return (
+18 -29
View File
@@ -15,44 +15,33 @@ import { BaseDialog, DialogRef, Switch, TooltipIcon } from '@/components/base'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
const buildInitialValues = (verge?: IVergeConfig | null) => ({
appLogLevel: verge?.app_log_level ?? 'warn',
appLogMaxSize: verge?.app_log_max_size ?? 128,
appLogMaxCount: verge?.app_log_max_count ?? 8,
autoCloseConnection: verge?.auto_close_connection ?? true,
autoCheckUpdate: verge?.auto_check_update ?? true,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 6,
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
autoDelayDetectionIntervalMinutes:
verge?.auto_delay_detection_interval_minutes ?? 5,
defaultLatencyTest: verge?.default_latency_test || '',
autoLogClean: verge?.auto_log_clean || 0,
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
})
export const MiscViewer = forwardRef<DialogRef>((props, ref) => { export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [values, setValues] = useState({ const [values, setValues] = useState(() => buildInitialValues(null))
appLogLevel: 'warn',
appLogMaxSize: 8,
appLogMaxCount: 12,
autoCloseConnection: true,
autoCheckUpdate: true,
enableBuiltinEnhanced: true,
proxyLayoutColumn: 6,
enableAutoDelayDetection: false,
autoDelayDetectionIntervalMinutes: 5,
defaultLatencyTest: '',
autoLogClean: 2,
defaultLatencyTimeout: 10000,
})
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
setValues({ setValues(buildInitialValues(verge))
appLogLevel: verge?.app_log_level ?? 'warn',
appLogMaxSize: verge?.app_log_max_size ?? 128,
appLogMaxCount: verge?.app_log_max_count ?? 8,
autoCloseConnection: verge?.auto_close_connection ?? true,
autoCheckUpdate: verge?.auto_check_update ?? true,
enableBuiltinEnhanced: verge?.enable_builtin_enhanced ?? true,
proxyLayoutColumn: verge?.proxy_layout_column || 6,
enableAutoDelayDetection: verge?.enable_auto_delay_detection ?? false,
autoDelayDetectionIntervalMinutes:
verge?.auto_delay_detection_interval_minutes ?? 5,
defaultLatencyTest: verge?.default_latency_test || '',
autoLogClean: verge?.auto_log_clean || 0,
defaultLatencyTimeout: verge?.default_latency_timeout || 10000,
})
}, },
close: () => setOpen(false), close: () => setOpen(false),
})) }))
+19 -34
View File
@@ -13,13 +13,7 @@ import {
Typography, Typography,
} from '@mui/material' } from '@mui/material'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import { import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@@ -41,6 +35,7 @@ import {
getSystemHostname, getSystemHostname,
} from '@/services/cmds' } from '@/services/cmds'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug' import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system' import getSystem from '@/utils/get-system'
@@ -87,6 +82,17 @@ const splitBypass = (value?: string) =>
.map((item) => item.trim()) .map((item) => item.trim())
.filter(Boolean) .filter(Boolean)
const buildInitialValues = (verge?: IVergeConfig | null) => ({
guard: verge?.enable_proxy_guard,
enable_bypass_check: verge?.enable_bypass_check ?? true,
bypass: verge?.system_proxy_bypass,
duration: verge?.proxy_guard_duration ?? 10,
use_default: verge?.use_default_bypass ?? true,
pac: verge?.proxy_auto_config,
pac_content: verge?.pac_file_content ?? DEFAULT_PAC,
proxy_host: verge?.proxy_host ?? '127.0.0.1',
})
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => { export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const systemName = getSystem() const systemName = getSystem()
@@ -116,23 +122,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
proxy_host, proxy_host,
} = verge ?? {} } = verge ?? {}
const [value, setValue] = useState({ const [value, setValue] = useState(() => buildInitialValues(null))
guard: enable_proxy_guard, const { data: embeddedServerPort } = useQuery({
enable_bypass_check: enable_bypass_check ?? true, queryKey: ['getEmbeddedServerPort'],
bypass: system_proxy_bypass, queryFn: getEmbeddedServerPort,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? '127.0.0.1',
}) })
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
null,
)
useEffect(() => {
getEmbeddedServerPort().then(setEmbeddedServerPort).catch(console.error)
}, [])
const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows]) const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows])
@@ -192,16 +186,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true) setOpen(true)
setValue({ setValue(buildInitialValues(verge))
guard: enable_proxy_guard,
enable_bypass_check: enable_bypass_check ?? true,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? '127.0.0.1',
})
fetchNetworkInterfaces() fetchNetworkInterfaces()
}, },
close: () => setOpen(false), close: () => setOpen(false),
@@ -339,7 +324,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
Promise.resolve().then(async () => { Promise.resolve().then(async () => {
try { try {
if (Object.keys(patch).length > 0) { if (Object.keys(patch).length > 0) {
mutateVerge({ ...verge, ...patch }, false) mutateVerge({ ...verge, ...patch })
} }
if (Object.keys(patch).length > 0) { if (Object.keys(patch).length > 0) {
await patchVerge(patch) await patchVerge(patch)
+21 -38
View File
@@ -1,4 +1,3 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material' import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material'
import { import {
Button, Button,
@@ -14,7 +13,6 @@ import {
} from '@mui/material' } from '@mui/material'
import { import {
forwardRef, forwardRef,
useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
@@ -76,34 +74,28 @@ const proxyOptionToken = ({ memberIndex, member }: TunnelProxyOption) =>
member.kind === 'node' ? member.node.recordId : member.ref.name member.kind === 'node' ? member.node.recordId : member.ref.name
}` }`
const buildInitialValues = (): TunnelFormValues => ({
localAddr: '',
localPort: '',
targetAddr: '',
targetPort: '',
network: 'tcp+udp',
group: '',
proxy: null,
})
export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => { export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { clash, mutateClash, patchClash } = useClash() const { clash, mutateClash, patchClash } = useClash()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [values, setValues] = useState<TunnelFormValues>({ const [values, setValues] = useState<TunnelFormValues>(buildInitialValues)
localAddr: '',
localPort: '',
targetAddr: '',
targetPort: '',
network: 'tcp+udp',
group: '',
proxy: null,
})
const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([]) const [draftTunnels, setDraftTunnels] = useState<TunnelEntry[]>([])
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setValues(() => ({ setValues(buildInitialValues())
localAddr: '',
localPort: '',
targetAddr: '',
targetPort: '',
network: 'tcp+udp',
group: '',
proxy: null,
}))
setDraftTunnels(() => clash?.tunnels ?? []) setDraftTunnels(() => clash?.tunnels ?? [])
setOpen(true) setOpen(true)
// 如果没有隧道,则自动展开 // 如果没有隧道,则自动展开
@@ -163,29 +155,20 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const selectedProxyToken = selectedProxyOption const selectedProxyToken = selectedProxyOption
? proxyOptionToken(selectedProxyOption) ? proxyOptionToken(selectedProxyOption)
: '' : ''
const selectedGroupExists = // A group or member missing from the proxy view reads as the default.
!values.group || proxyGroups.some(({ name }) => name === values.group) const effectiveGroup = proxyGroups.some(({ name }) => name === values.group)
? values.group
: ''
const effectiveProxy = selectedProxyOption ? values.proxy : null
const latestProxyStateRef = useRef({ const latestProxyStateRef = useRef({
binding: values.proxy, binding: effectiveProxy,
options: proxyOptions, options: proxyOptions,
}) })
latestProxyStateRef.current = { latestProxyStateRef.current = {
binding: values.proxy, binding: effectiveProxy,
options: proxyOptions, options: proxyOptions,
} }
useEffect(() => {
if (selectedGroupExists && (!values.proxy || selectedProxyOption)) return
setValues((current) => {
if (current.group !== values.group || current.proxy !== values.proxy) {
return current
}
return selectedGroupExists
? { ...current, proxy: null }
: { ...current, group: '', proxy: null }
})
}, [selectedGroupExists, selectedProxyOption, values.group, values.proxy])
const handleSave = async () => { const handleSave = async () => {
try { try {
await patchClash({ tunnels: draftTunnels }) await patchClash({ tunnels: draftTunnels })
@@ -496,7 +479,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<Select <Select
size="small" size="small"
sx={{ width: 200, '> div': { py: '7.5px' } }} sx={{ width: 200, '> div': { py: '7.5px' } }}
value={values.group} value={effectiveGroup}
displayEmpty displayEmpty
onChange={(e) => { onChange={(e) => {
const nextGroup = e.target.value as string const nextGroup = e.target.value as string
@@ -575,7 +558,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
: null, : null,
})) }))
}} }}
disabled={!values.group} // 没选组就禁用 disabled={!effectiveGroup} // 没选组就禁用
> >
<MenuItem value=""> <MenuItem value="">
{t('settings.sections.clash.form.fields.tunnels.default')} {t('settings.sections.clash.form.fields.tunnels.default')}
+40 -42
View File
@@ -1,14 +1,8 @@
import { alpha, Box, Button, LinearProgress } from '@mui/material' import { alpha, Box, Button, LinearProgress } from '@mui/material'
import type { Update } from '@tauri-apps/plugin-updater'
import { useLockFn } from 'ahooks' import { useLockFn } from 'ahooks'
import type { Ref } from 'react' import type { Ref } from 'react'
import { import { lazy, Suspense, useImperativeHandle, useMemo, useState } from 'react'
lazy,
Suspense,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { Options as ReactMarkdownOptions } from 'react-markdown' import type { Options as ReactMarkdownOptions } from 'react-markdown'
@@ -180,12 +174,16 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
const updateState = useUpdateState() const updateState = useUpdateState()
const setUpdateState = useSetUpdateState() const setUpdateState = useSetUpdateState()
const { updateInfo } = useUpdate() const { updateInfo: detectedUpdate, checkUpdate } = useUpdate()
const [refreshedUpdate, setRefreshedUpdate] = useState<Update | null>()
const updateInfo =
refreshedUpdate === undefined ? detectedUpdate : refreshedUpdate
const [downloaded, setDownloaded] = useState(0) const [download, setDownload] = useState({
const [total, setTotal] = useState(0) downloaded: 0,
const downloadedRef = useRef(0) total: 0,
const totalRef = useRef(0) })
const { downloaded, total } = download
const openUrlWithNotice = (url: string) => { const openUrlWithNotice = (url: string) => {
void openExternalUrl(url).catch(showNotice.error) void openExternalUrl(url).catch(showNotice.error)
@@ -197,7 +195,10 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
}, [downloaded, total]) }, [downloaded, total])
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => setOpen(true), open: () => {
setRefreshedUpdate(undefined)
setOpen(true)
},
close: () => setOpen(false), close: () => setOpen(false),
})) }))
@@ -242,32 +243,21 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
} }
if (updateState) return if (updateState) return
setUpdateState(true) setUpdateState(true)
setDownloaded(0) setDownload({ downloaded: 0, total: 0 })
setTotal(0)
downloadedRef.current = 0
totalRef.current = 0
const onDownloadEvent = (event: DownloadEvent) => { const onDownloadEvent = (event: DownloadEvent) => {
if (event.event === 'Started') { if (event.event === 'Started') {
const contentLength = event.data.contentLength ?? 0 setDownload({ downloaded: 0, total: event.data.contentLength ?? 0 })
totalRef.current = contentLength } else if (event.event === 'Progress') {
setTotal(contentLength) setDownload((prev) => ({
setDownloaded(0) ...prev,
downloadedRef.current = 0 downloaded: prev.downloaded + event.data.chunkLength,
return }))
} } else if (event.event === 'Finished') {
setDownload((prev) => ({
if (event.event === 'Progress') { ...prev,
setDownloaded((prev) => { total: prev.total || prev.downloaded,
const next = prev + event.data.chunkLength }))
downloadedRef.current = next
return next
})
}
if (event.event === 'Finished' && totalRef.current === 0) {
totalRef.current = downloadedRef.current
setTotal(downloadedRef.current)
} }
} }
@@ -279,7 +269,18 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
errorNotice: false, errorNotice: false,
}, },
) )
if (result.ok && result.value) { if (result.ok && result.value === 'update_changed') {
const { data } = await checkUpdate()
setRefreshedUpdate(data)
if (data) {
showNotice.info('settings.modals.update.messages.available')
} else {
setOpen(false)
showNotice.info(
'settings.components.verge.advanced.notifications.latestVersion',
)
}
} else if (result.ok && result.value === 'installed') {
await mutate(() => restartApp(), { await mutate(() => restartApp(), {
id: 'restart-app', id: 'restart-app',
errorNotice: false, errorNotice: false,
@@ -289,10 +290,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
showNotice.error(err) showNotice.error(err)
} finally { } finally {
setUpdateState(false) setUpdateState(false)
setDownloaded(0) setDownload({ downloaded: 0, total: 0 })
setTotal(0)
downloadedRef.current = 0
totalRef.current = 0
} }
}) })
@@ -50,21 +50,21 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
const handleAdd = useLockFn(async (value: string) => { const handleAdd = useLockFn(async (value: string) => {
const newList = [...webUIList, value] const newList = [...webUIList, value]
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false) mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
await patchVerge({ web_ui_list: newList }) await patchVerge({ web_ui_list: newList })
}) })
const handleChange = useLockFn(async (index: number, value?: string) => { const handleChange = useLockFn(async (index: number, value?: string) => {
const newList = [...webUIList] const newList = [...webUIList]
newList[index] = value ?? '' newList[index] = value ?? ''
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false) mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
await patchVerge({ web_ui_list: newList }) await patchVerge({ web_ui_list: newList })
}) })
const handleDelete = useLockFn(async (index: number) => { const handleDelete = useLockFn(async (index: number) => {
const newList = [...webUIList] const newList = [...webUIList]
newList.splice(index, 1) newList.splice(index, 1)
mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old), false) mutateVerge((old) => (old ? { ...old, web_ui_list: newList } : old))
await patchVerge({ web_ui_list: newList }) await patchVerge({ web_ui_list: newList })
}) })
+3 -13
View File
@@ -1,14 +1,12 @@
import React, { useRef } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DialogRef, Switch, TooltipIcon } from '@/components/base' import { Switch, TooltipIcon } from '@/components/base'
import ProxyControlSwitches from '@/components/shared/proxy-control-switches' import ProxyControlSwitches from '@/components/shared/proxy-control-switches'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import { GuardState } from './mods/guard-state' import { GuardState } from './mods/guard-state'
import { SettingList, SettingItem } from './mods/setting-comp' import { SettingList, SettingItem } from './mods/setting-comp'
import { SysproxyViewer } from './mods/sysproxy-viewer'
import { TunViewer } from './mods/tun-viewer'
interface Props { interface Props {
onError?: (err: Error) => void onError?: (err: Error) => void
@@ -21,22 +19,16 @@ const SettingSystem = ({ onError }: Props) => {
const { enable_auto_launch, enable_silent_start } = verge ?? {} const { enable_auto_launch, enable_silent_start } = verge ?? {}
const sysproxyRef = useRef<DialogRef>(null)
const tunRef = useRef<DialogRef>(null)
const onSwitchFormat = ( const onSwitchFormat = (
_e: React.ChangeEvent<HTMLInputElement>, _e: React.ChangeEvent<HTMLInputElement>,
value: boolean, value: boolean,
) => value ) => value
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false) mutateVerge({ ...verge, ...patch })
} }
return ( return (
<SettingList title={t('settings.sections.system.title')}> <SettingList title={t('settings.sections.system.title')}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
<ProxyControlSwitches <ProxyControlSwitches
label={t('settings.sections.system.toggles.tunMode')} label={t('settings.sections.system.toggles.tunMode')}
onError={onError} onError={onError}
@@ -58,8 +50,6 @@ const SettingSystem = ({ onError }: Props) => {
}} }}
onGuard={async (e) => { onGuard={async (e) => {
try { try {
// 先触发UI更新立即看到反馈
onChangeData({ enable_auto_launch: e })
await patchVerge({ enable_auto_launch: e }) await patchVerge({ enable_auto_launch: e })
return Promise.resolve() return Promise.resolve()
} catch (error) { } catch (error) {
@@ -72,7 +72,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
const backupRef = useRef<DialogRef>(null) const backupRef = useRef<DialogRef>(null)
const onChangeData = (patch: any) => { const onChangeData = (patch: any) => {
mutateVerge({ ...verge, ...patch }, false) mutateVerge({ ...verge, ...patch })
} }
const onCopyClashEnv = useCallback(async () => { const onCopyClashEnv = useCallback(async () => {
@@ -163,7 +163,7 @@ const ProxyControlSwitches = ({
}) })
return false return false
} }
mutateVerge({ ...verge, enable_tun_mode: value }, false) mutateVerge({ ...verge, enable_tun_mode: value })
await patchVerge({ enable_tun_mode: value }) await patchVerge({ enable_tun_mode: value })
} }
@@ -229,8 +229,8 @@ const ProxyControlSwitches = ({
/> />
)} )}
<SysproxyViewer ref={sysproxyRef} /> {isSystemProxyMode && <SysproxyViewer ref={sysproxyRef} />}
<TunViewer ref={tunRef} /> {isTunMode && <TunViewer ref={tunRef} />}
</Box> </Box>
) )
} }
@@ -1,301 +0,0 @@
import {
ErrorOutlineRounded,
RefreshRounded,
BugReportRounded,
} from '@mui/icons-material'
import { Box, Typography, Button, Alert, Collapse } from '@mui/material'
import React, { Component, ErrorInfo, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
children: ReactNode
fallbackComponent?: ReactNode
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface State {
hasError: boolean
error: Error | null
errorInfo: ErrorInfo | null
showDetails: boolean
}
export class TrafficErrorBoundary extends Component<Props, State> {
private retryCount = 0
private maxRetries = 3
constructor(props: Props) {
super(props)
this.state = {
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
}
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('[TrafficErrorBoundary] 捕获到组件错误:', error, errorInfo)
this.setState({
error,
errorInfo,
})
if (this.props.onError) {
this.props.onError(error, errorInfo)
}
this.reportError(error, errorInfo)
}
private reportError = (error: Error, errorInfo: ErrorInfo) => {
const errorReport = {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
}
console.error('[TrafficErrorBoundary] 错误报告:', errorReport)
}
private handleRetry = () => {
if (this.retryCount < this.maxRetries) {
this.retryCount++
console.log(
`[TrafficErrorBoundary] 尝试重试 (${this.retryCount}/${this.maxRetries})`,
)
this.setState({
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
})
} else {
console.warn('[TrafficErrorBoundary] 已达到最大重试次数')
}
}
private handleRefresh = () => {
window.location.reload()
}
private toggleDetails = () => {
this.setState((prev) => ({ showDetails: !prev.showDetails }))
}
render() {
if (this.state.hasError) {
if (this.props.fallbackComponent) {
return this.props.fallbackComponent
}
return (
<TrafficErrorFallback
error={this.state.error}
errorInfo={this.state.errorInfo}
showDetails={this.state.showDetails}
canRetry={this.retryCount < this.maxRetries}
retryCount={this.retryCount}
maxRetries={this.maxRetries}
onRetry={this.handleRetry}
onRefresh={this.handleRefresh}
onToggleDetails={this.toggleDetails}
/>
)
}
return this.props.children
}
}
interface TrafficErrorFallbackProps {
error: Error | null
errorInfo: ErrorInfo | null
showDetails: boolean
canRetry: boolean
retryCount: number
maxRetries: number
onRetry: () => void
onRefresh: () => void
onToggleDetails: () => void
}
const TrafficErrorFallback: React.FC<TrafficErrorFallbackProps> = ({
error,
errorInfo,
showDetails,
canRetry,
retryCount,
maxRetries,
onRetry,
onRefresh,
onToggleDetails,
}) => {
const { t } = useTranslation()
return (
<Box
sx={{
p: 2,
minHeight: 200,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
border: '1px dashed',
borderColor: 'error.main',
borderRadius: 2,
bgcolor: 'error.light',
color: 'error.contrastText',
}}
>
<ErrorOutlineRounded sx={{ fontSize: 48, mb: 2, color: 'error.main' }} />
<Typography variant="h6" gutterBottom>
{t('shared.feedback.errors.trafficStats')}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 2, textAlign: 'center' }}
>
{t('shared.feedback.errors.trafficStatsDescription')}
</Typography>
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
<Typography variant="body2">
<strong>{t('shared.feedback.errors.label')}:</strong>{' '}
{error instanceof Error
? error.message
: t('shared.feedback.errors.unknown')}
</Typography>
{retryCount > 0 && (
<Typography variant="caption" sx={{ mt: 1, display: 'block' }}>
{t('shared.labels.retryAttempts')}: {retryCount}/{maxRetries}
</Typography>
)}
</Alert>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
{canRetry && (
<Button
variant="contained"
color="primary"
startIcon={<RefreshRounded />}
onClick={onRetry}
size="small"
>
{t('shared.actions.retry')}
</Button>
)}
<Button variant="outlined" onClick={onRefresh} size="small">
{t('shared.actions.refreshPage')}
</Button>
<Button
variant="text"
startIcon={<BugReportRounded />}
onClick={onToggleDetails}
size="small"
>
{showDetails
? t('shared.actions.hideDetails')
: t('shared.actions.showDetails')}
</Button>
</Box>
<Collapse in={showDetails} sx={{ width: '100%', maxWidth: 600 }}>
<Box
sx={{
p: 2,
bgcolor: 'background.paper',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="subtitle2" gutterBottom>
{t('shared.feedback.errors.details')}:
</Typography>
<Typography
variant="caption"
component="pre"
sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.75rem',
color: 'text.secondary',
}}
>
{error?.stack}
</Typography>
{errorInfo?.componentStack && (
<>
<Typography variant="subtitle2" gutterBottom sx={{ mt: 2 }}>
{t('shared.feedback.errors.componentStack')}:
</Typography>
<Typography
variant="caption"
component="pre"
sx={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'monospace',
fontSize: '0.75rem',
color: 'text.secondary',
}}
>
{errorInfo.componentStack}
</Typography>
</>
)}
</Box>
</Collapse>
</Box>
)
}
export const LightweightTrafficErrorBoundary: React.FC<{
children: ReactNode
}> = ({ children }) => {
const { t } = useTranslation()
return (
<TrafficErrorBoundary
fallbackComponent={
<Box
sx={{
p: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: 60,
bgcolor: 'error.light',
borderRadius: 1,
color: 'error.contrastText',
}}
>
<ErrorOutlineRounded sx={{ mr: 1, fontSize: 20 }} />
<Typography variant="caption">
{t('shared.feedback.errors.trafficUnavailable')}
</Typography>
</Box>
}
>
{children}
</TrafficErrorBoundary>
)
}
+18 -4
View File
@@ -7,6 +7,10 @@ import { useTranslation } from 'react-i18next'
import { BaseDialog } from '@/components/base' import { BaseDialog } from '@/components/base'
import { useVerge } from '@/hooks/use-verge' import { useVerge } from '@/hooks/use-verge'
import type {
IVergeTestItem_Deserialize,
IVergeTestItem_Serialize,
} from '@/services/bindings'
import { showNotice } from '@/services/notice-service' import { showNotice } from '@/services/notice-service'
interface Props { interface Props {
@@ -27,7 +31,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { verge, patchVerge } = useVerge() const { verge, patchVerge } = useVerge()
const testList = verge?.test_list ?? [] const testList = verge?.test_list ?? []
const { control, ...formIns } = useForm<IVergeTestItem>({ const { control, ...formIns } = useForm<IVergeTestItem_Deserialize>({
defaultValues: { defaultValues: {
name: '', name: '',
icon: '', icon: '',
@@ -37,11 +41,16 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const patchTestList = async ( const patchTestList = async (
uid: string, uid: string,
patch: Partial<IVergeTestItem>, patch: Partial<IVergeTestItem_Deserialize>,
) => { ) => {
const newList = testList.map((x) => { const newList = testList.map((x) => {
if (x.uid === uid) { if (x.uid === uid) {
return { ...x, ...patch } return {
uid: x.uid,
name: patch.name === undefined ? x.name : patch.name,
icon: patch.icon === undefined ? x.icon : patch.icon,
url: patch.url === undefined ? x.url : patch.url,
}
} }
return x return x
}) })
@@ -77,7 +86,12 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
if (openType === 'new') { if (openType === 'new') {
const uid = nanoid() const uid = nanoid()
const item = { ...form, uid } const item: IVergeTestItem_Serialize = {
uid,
name: form.name ?? null,
icon: form.icon ?? null,
url: form.url ?? null,
}
const newList = [...testList, item] const newList = [...testList, item]
await patchVerge({ test_list: newList }) await patchVerge({ test_list: newList })
onChange(uid) onChange(uid)
+2 -4
View File
@@ -1,5 +1,4 @@
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { useMemo } from 'react'
import { downloadIconCache } from '@/services/cmds' import { downloadIconCache } from '@/services/cmds'
import { useQuery } from '@/services/query-client' import { useQuery } from '@/services/query-client'
@@ -23,9 +22,8 @@ export const useIconCache = ({
const iconValue = icon?.trim() ?? '' const iconValue = icon?.trim() ?? ''
const cacheKeyValue = cacheKey?.trim() ?? '' const cacheKeyValue = cacheKey?.trim() ?? ''
const isEnabled = useMemo(() => { const isEnabled =
return enabled && iconValue.startsWith('http') && cacheKeyValue !== '' enabled && iconValue.startsWith('http') && cacheKeyValue !== ''
}, [enabled, iconValue, cacheKeyValue])
const { data } = useQuery({ const { data } = useQuery({
queryKey: ['icon-cache', iconValue, cacheKeyValue], queryKey: ['icon-cache', iconValue, cacheKeyValue],
+4 -8
View File
@@ -54,8 +54,7 @@ export const useLogData = () => {
const hasLoadedInitialLogsRef = useRef(false) const hasLoadedInitialLogsRef = useRef(false)
const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({ const { response, refresh, setData } = useMihomoWsSubscription<ILogItem[]>({
storageKey: 'mihomo_logs_date', subscriptionKey: enableLog ? 'getClashLog' : null,
buildSubscriptKey: (date) => (enableLog ? `getClashLog-${date}` : null),
fallbackData: [], fallbackData: [],
connect: () => MihomoWebSocket.connect_logs(logLevel), connect: () => MihomoWebSocket.connect_logs(logLevel),
setupHandlers: ({ next, scheduleReconnect, isMounted }) => { setupHandlers: ({ next, scheduleReconnect, isMounted }) => {
@@ -77,14 +76,13 @@ export const useLogData = () => {
} }
const pendingLogs = buffer.splice(0, buffer.length) const pendingLogs = buffer.splice(0, buffer.length)
flushTimeStr = null flushTimeStr = null
next(null, (current) => appendLogs(current, pendingLogs)) next((current) => appendLogs(current, pendingLogs))
flushTimer = null flushTimer = null
} }
return { return {
handleMessage: (data) => { handleMessage: (data) => {
if (data.startsWith('Websocket error')) { if (data.startsWith('Websocket error')) {
next(data)
void scheduleReconnect() void scheduleReconnect()
return return
} }
@@ -108,9 +106,7 @@ export const useLogData = () => {
if (!flushTimer) { if (!flushTimer) {
flushTimer = setTimeout(flush, FLUSH_DELAY_MS) flushTimer = setTimeout(flush, FLUSH_DELAY_MS)
} }
} catch (error) { } catch {}
next(error)
}
}, },
async onConnected() { async onConnected() {
if (hasLoadedInitialLogsRef.current) { if (hasLoadedInitialLogsRef.current) {
@@ -119,7 +115,7 @@ export const useLogData = () => {
const logs = await getClashLogs() const logs = await getClashLogs()
hasLoadedInitialLogsRef.current = true hasLoadedInitialLogsRef.current = true
if (isMounted()) { if (isMounted()) {
next(null, (current) => { next((current) => {
if (!current || current.length === 0) { if (!current || current.length === 0) {
return clampLogs(filterLogsByLevel(logs, allowedTypes)) return clampLogs(filterLogsByLevel(logs, allowedTypes))
} }
+3 -7
View File
@@ -33,15 +33,13 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
const enabled = options?.enabled ?? true const enabled = options?.enabled ?? true
const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({ const { response, refresh } = useMihomoWsSubscription<IMemoryUsageItem>({
storageKey: 'mihomo_memory_date', subscriptionKey: enabled ? 'getClashMemory' : null,
buildSubscriptKey: (date) => (enabled ? `getClashMemory-${date}` : null),
fallbackData: FALLBACK_MEMORY_USAGE, fallbackData: FALLBACK_MEMORY_USAGE,
connect: () => MihomoWebSocket.connect_memory(), connect: () => MihomoWebSocket.connect_memory(),
throttleMs: 500, throttleMs: 500,
setupHandlers: ({ next, scheduleReconnect }) => ({ setupHandlers: ({ next, scheduleReconnect }) => ({
handleMessage: (data) => { handleMessage: (data) => {
if (data.startsWith('Websocket error')) { if (data.startsWith('Websocket error')) {
next(data, FALLBACK_MEMORY_USAGE)
void scheduleReconnect() void scheduleReconnect()
return return
} }
@@ -51,10 +49,8 @@ export const useMemoryData = (options?: { enabled?: boolean }) => {
if (shouldSkipDuplicateMemory(parsed)) { if (shouldSkipDuplicateMemory(parsed)) {
return return
} }
next(null, parsed) next(parsed)
} catch (error) { } catch {}
next(error, FALLBACK_MEMORY_USAGE)
}
}, },
}), }),
}) })
+32 -68
View File
@@ -1,12 +1,7 @@
import { useLocalStorage } from 'foxact/use-local-storage' import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api' import { type Message, type MihomoWebSocket } from 'tauri-plugin-mihomo-api'
import { import { setCacheData, useQuery } from '@/services/query-client'
removeCacheData,
setCacheData,
useQuery,
} from '@/services/query-client'
const RECONNECT_DELAY_MS = 1000 const RECONNECT_DELAY_MS = 1000
@@ -31,7 +26,6 @@ interface SharedSubscriptionEntry {
const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>() const sharedSubscriptions = new Map<string, SharedSubscriptionEntry>()
const subscriptionSnapshots = new Map<string, unknown>() const subscriptionSnapshots = new Map<string, unknown>()
const initialSubscriptionDate = Date.now()
const getSubscriptionSnapshot = <T>(key: string) => const getSubscriptionSnapshot = <T>(key: string) =>
subscriptionSnapshots.get(key) as T | undefined subscriptionSnapshots.get(key) as T | undefined
@@ -136,15 +130,13 @@ const createSharedSubscriptionEntry = (
return entry return entry
} }
type Updater<T> = (current?: T) => T
/** /**
* Mirrors SWR's MutatorCallback: consumers can pass either a plain value or a * Pass either a plain value or a functional updater; the updater is resolved
* functional updater `(current?: T) => T`. The functional form is resolved * against the current subscription snapshot before the cache is written.
* against the current subscription snapshot before updating SWR.
*/ */
type NextFn<T> = ( type NextFn<T> = (data: T | Updater<T>) => void
error?: any,
data?: T | ((current?: T) => T | undefined),
) => void
interface HandlerContext<T> { interface HandlerContext<T> {
next: NextFn<T> next: NextFn<T>
@@ -159,8 +151,7 @@ interface HandlerResult {
} }
interface UseMihomoWsSubscriptionOptions<T> { interface UseMihomoWsSubscriptionOptions<T> {
storageKey: string subscriptionKey: string | null
buildSubscriptKey: (date: number) => string | null
fallbackData: T fallbackData: T
connect: () => Promise<MihomoWebSocket> connect: () => Promise<MihomoWebSocket>
/** /**
@@ -178,18 +169,12 @@ interface UseMihomoWsSubscriptionOptions<T> {
export const useMihomoWsSubscription = <T>( export const useMihomoWsSubscription = <T>(
options: UseMihomoWsSubscriptionOptions<T>, options: UseMihomoWsSubscriptionOptions<T>,
) => { ) => {
const { const { subscriptionKey, fallbackData, connect, throttleMs, setupHandlers } =
storageKey, options
buildSubscriptKey,
fallbackData,
connect,
throttleMs,
setupHandlers,
} = options
const [date, setDate] = useLocalStorage(storageKey, initialSubscriptionDate) const subscriptionCacheKey = subscriptionKey
const subscriptKey = buildSubscriptKey(date) ? `$sub$${subscriptionKey}`
const subscriptionCacheKey = subscriptKey ? `$sub$${subscriptKey}` : null : null
const lastSubscriptionCacheKeyRef = useRef<string | null>(null) const lastSubscriptionCacheKeyRef = useRef<string | null>(null)
if (subscriptionCacheKey) { if (subscriptionCacheKey) {
lastSubscriptionCacheKeyRef.current = subscriptionCacheKey lastSubscriptionCacheKeyRef.current = subscriptionCacheKey
@@ -197,20 +182,9 @@ export const useMihomoWsSubscription = <T>(
const responseCacheKey = const responseCacheKey =
subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current subscriptionCacheKey ?? lastSubscriptionCacheKeyRef.current
const resolveNextData = useCallback( // Bumping the epoch re-runs the subscription effect, rebuilding the shared
( // entry (and its captured `connect`/handler closures) under the same key.
data: T | ((current?: T) => T | undefined) | undefined, const [refreshEpoch, setRefreshEpoch] = useState(0)
cacheKey: string,
): T => {
if (typeof data === 'function') {
const updater = data as (current?: T) => T | undefined
const current = getSubscriptionSnapshot<T>(cacheKey)
return updater(current) ?? fallbackData
}
return data ?? fallbackData
},
[fallbackData],
)
const response = useQuery<T>({ const response = useQuery<T>({
queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'], queryKey: responseCacheKey ? [responseCacheKey] : ['$sub$__disabled__'],
@@ -238,44 +212,35 @@ export const useMihomoWsSubscription = <T>(
let throttleCleanup: (() => void) | undefined let throttleCleanup: (() => void) | undefined
let wrappedNext: NextFn<T> let wrappedNext: NextFn<T>
const baseNext: NextFn<T> = (error, data) => { const baseNext: NextFn<T> = (data) => {
if (error !== undefined && error !== null) { const resolved =
return typeof data === 'function'
} ? (data as Updater<T>)(
if (data === undefined) return getSubscriptionSnapshot<T>(subscriptionCacheKey),
const resolved = resolveNextData(data, subscriptionCacheKey) )
: (data as T)
writeSubscriptionSnapshot(subscriptionCacheKey, resolved) writeSubscriptionSnapshot(subscriptionCacheKey, resolved)
} }
if (throttleMs && throttleMs > 0) { if (throttleMs && throttleMs > 0) {
let pendingData: T | ((current?: T) => T | undefined) | undefined let pendingData: T | Updater<T> | undefined
let hasPending = false
let timerId: ReturnType<typeof setTimeout> | null = null let timerId: ReturnType<typeof setTimeout> | null = null
const flush = () => { const flush = () => {
timerId = null timerId = null
if (hasPending) { if (pendingData !== undefined) {
const data = pendingData const data = pendingData
pendingData = undefined pendingData = undefined
hasPending = false baseNext(data)
baseNext(undefined, data)
} }
} }
wrappedNext = ( wrappedNext = (data) => {
error?: any,
data?: T | ((current?: T) => T | undefined),
) => {
if (error !== undefined && error !== null) {
baseNext(error, data)
return
}
if (!timerId) { if (!timerId) {
baseNext(undefined, data) baseNext(data)
timerId = setTimeout(flush, throttleMs) timerId = setTimeout(flush, throttleMs)
} else { } else {
pendingData = data pendingData = data
hasPending = true
} }
} }
@@ -341,15 +306,14 @@ export const useMihomoWsSubscription = <T>(
} }
// eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps
}, [subscriptionCacheKey]) }, [subscriptionCacheKey, refreshEpoch])
const refresh = useCallback(() => { const refresh = useCallback(() => {
if (subscriptionCacheKey) { if (subscriptionCacheKey) {
subscriptionSnapshots.delete(subscriptionCacheKey) writeSubscriptionSnapshot(subscriptionCacheKey, fallbackData)
void removeCacheData([subscriptionCacheKey])
} }
setDate(Date.now()) setRefreshEpoch((epoch) => epoch + 1)
}, [subscriptionCacheKey, setDate]) }, [subscriptionCacheKey, fallbackData])
const setData = useCallback( const setData = useCallback(
(data: T) => { (data: T) => {
+6 -1
View File
@@ -17,7 +17,12 @@ const PRESET_PROXY_NAMES = [
'COMPATIBLE', 'COMPATIBLE',
] ]
const identity = (_: DelayUpdate, next: DelayUpdate): DelayUpdate => next const identity = (previous: DelayUpdate, next: DelayUpdate): DelayUpdate =>
previous.delay === next.delay &&
previous.elapsed === next.elapsed &&
previous.updatedAt === next.updatedAt
? previous
: next
const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 } const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 }
+2 -2
View File
@@ -35,8 +35,8 @@ it('applies and records independent group selections while another group is pend
const secondSuccess = vi.fn() const secondSuccess = vi.fn()
const first = useProxySelection({ onSuccess: firstSuccess }) const first = useProxySelection({ onSuccess: firstSuccess })
const second = useProxySelection({ onSuccess: secondSuccess }) const second = useProxySelection({ onSuccess: secondSuccess })
first.changeProxy('Group A', 'Node A') first.select('Group A', 'Node A')
second.changeProxy('Group B', 'Node B') second.select('Group B', 'Node B')
await vi.waitFor(() => await vi.waitFor(() =>
expect(record).toHaveBeenCalledWith('Group B', 'Node B'), expect(record).toHaveBeenCalledWith('Group B', 'Node B'),
) )
+4 -28
View File
@@ -35,7 +35,6 @@ const cleanupConnections = async (previousProxy: string) => {
interface ProxySelectionOptions { interface ProxySelectionOptions {
onSuccess?: () => void onSuccess?: () => void
onError?: (error: any) => void onError?: (error: any) => void
enableConnectionCleanup?: boolean
} }
interface ProxyChangeRequest { interface ProxyChangeRequest {
@@ -53,7 +52,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
const pendingRequestRef = useRef<ProxyChangeRequest | null>(null) const pendingRequestRef = useRef<ProxyChangeRequest | null>(null)
const isProcessingRef = useRef(false) const isProcessingRef = useRef(false)
const { onSuccess, onError, enableConnectionCleanup = true } = options const { onSuccess, onError } = options
const autoCloseConnection = verge?.auto_close_connection ?? false const autoCloseConnection = verge?.auto_close_connection ?? false
@@ -98,7 +97,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
) )
} }
if (enableConnectionCleanup && autoCloseConnection && previousProxy) { if (autoCloseConnection && previousProxy) {
void cleanupConnections(previousProxy) void cleanupConnections(previousProxy)
} }
} catch (error) { } catch (error) {
@@ -112,7 +111,6 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
[ [
autoCloseConnection, autoCloseConnection,
forgetSelection, forgetSelection,
enableConnectionCleanup,
onError, onError,
onSuccess, onSuccess,
recordSelection, recordSelection,
@@ -138,7 +136,7 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
} }
}, [executeChange]) }, [executeChange])
const changeProxy = useCallback( const select = useCallback(
( (
groupName: string, groupName: string,
proxyName: string, proxyName: string,
@@ -156,27 +154,5 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
[flushChangeQueue], [flushChangeQueue],
) )
const handleSelectChange = useCallback( return { select }
(groupName: string, previousProxy?: string, fixed?: string) =>
(event: { target: { value: string } }) => {
changeProxy(groupName, event.target.value, previousProxy, fixed)
},
[changeProxy],
)
const handleProxyGroupChange = useCallback(
(
group: { name: string; now?: string | null; fixed?: string | null },
proxy: { name: string },
) => {
changeProxy(group.name, proxy.name, group.now!, group.fixed!)
},
[changeProxy],
)
return {
changeProxy,
handleSelectChange,
handleProxyGroupChange,
}
} }

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