Compare commits

...
Author SHA1 Message Date
nrenvo a15ff6b448 ci: drop markdown from the frontend check paths (#8492) 2026-11-04 14:44:12 +00:00
nrenvo dae542f772 ci: regenerate the i18n types and check them for drift (#8494) 2026-11-04 14:43:16 +00:00
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
51 changed files with 1264 additions and 1075 deletions
+9 -1
View File
@@ -4,13 +4,13 @@ on:
pull_request:
paths:
- "src/**"
- "scripts/**"
- "**/*.js"
- "**/*.ts"
- "**/*.tsx"
- "**/*.css"
- "**/*.scss"
- "**/*.json"
- "**/*.md"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "vite.config.*"
@@ -41,6 +41,14 @@ jobs:
- run: pnpm install --frozen-lockfile
- name: Check generated i18n types are up to date
run: |
pnpm i18n:types
if ! git diff --exit-code -- src/types/generated; then
echo "::error::src/types/generated is stale; run pnpm i18n:types and commit the result"
exit 1
fi
- name: Run Prettier check
run: pnpm format:check
Generated
+188 -151
View File
@@ -150,7 +150,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"parking_lot 0.12.5",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
@@ -159,9 +159,9 @@ dependencies = [
[[package]]
name = "arc-swap"
version = "1.9.2"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e"
dependencies = [
"rustversion",
]
@@ -1089,7 +1089,6 @@ dependencies = [
"async-trait",
"backon",
"base64 0.23.1",
"bitflags 2.13.2",
"boa_engine",
"bytes",
"chrono",
@@ -1100,7 +1099,6 @@ dependencies = [
"clash-verge-media-unlock",
"clash-verge-signal",
"clash_verge_service_ipc",
"criterion",
"dark-light",
"deelevate",
"dirs 7.0.0",
@@ -1117,9 +1115,8 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"open",
"parking_lot",
"parking_lot 0.13.0",
"percent-encoding",
"regex",
"reqwest 0.13.5",
"reqwest_dav",
"rfd",
@@ -1149,7 +1146,6 @@ dependencies = [
"tauri-plugin-mihomo",
"tauri-plugin-notification",
"tauri-plugin-opener",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-plugin-wdio-webdriver",
@@ -1157,7 +1153,6 @@ dependencies = [
"tauri-specta",
"tokio",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"tracing",
"warp",
@@ -1175,7 +1170,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"criterion",
"parking_lot",
"parking_lot 0.13.0",
"tokio",
]
@@ -1198,7 +1193,7 @@ dependencies = [
"anyhow",
"flexi_logger",
"log",
"parking_lot",
"parking_lot 0.13.0",
"tracing-estuary",
]
@@ -1255,8 +1250,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "facfbc410a385e0b0cbca8179dd9e363ed70e8455960bf7545137e90ca753b66"
dependencies = [
"crossbeam-utils",
"lock_api",
"parking_lot_core",
"lock_api 0.4.14",
"parking_lot_core 0.9.12",
"polonius-the-crab",
]
@@ -1333,7 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1767,8 +1762,18 @@ version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.23.0",
"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]]
@@ -1784,17 +1789,41 @@ dependencies = [
"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]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"darling_core 0.23.0",
"quote",
"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]]
name = "dashmap"
version = "6.2.1"
@@ -1804,9 +1833,9 @@ dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown 0.14.5",
"lock_api",
"lock_api 0.4.14",
"once_cell",
"parking_lot_core",
"parking_lot_core 0.9.12",
]
[[package]]
@@ -1884,9 +1913,9 @@ dependencies = [
[[package]]
name = "devtools-core"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c53d416e473377276435d49d731a75d253b96ee4339831cd85653702b94d8298"
checksum = "c9e85926bda763dd434fcfbcfb24af4b2c1a8c3e3c34bcd5d844c48f655cca51"
dependencies = [
"bytes",
"devtools-wire-format",
@@ -2014,7 +2043,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -2273,7 +2302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2789,7 +2818,7 @@ dependencies = [
"gobject-sys 0.21.5",
"libc",
"system-deps 7.0.8",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3343,7 +3372,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.2",
"windows-core 0.62.2",
]
[[package]]
@@ -3357,12 +3386,12 @@ dependencies = [
[[package]]
name = "ico"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167"
dependencies = [
"byteorder",
"png 0.17.16",
"png",
]
[[package]]
@@ -3548,7 +3577,7 @@ dependencies = [
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
"png",
"tiff",
]
@@ -3857,7 +3886,7 @@ dependencies = [
"http",
"httparse",
"libc",
"parking_lot",
"parking_lot 0.12.5",
"path-tree",
"pin-project-lite",
"rand 0.10.2",
@@ -4018,10 +4047,19 @@ dependencies = [
]
[[package]]
name = "log"
version = "0.4.34"
name = "lock_api"
version = "0.5.0"
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]]
name = "lru-slab"
@@ -4031,9 +4069,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
version = "0.23.0"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa"
checksum = "490750560fa7851fbbf5d3ab1a8750f09350ed361ed8aed85b1e1e40587812ee"
dependencies = [
"sha2 0.11.1",
]
@@ -4211,10 +4249,10 @@ dependencies = [
"objc2-core-foundation",
"objc2-foundation",
"once_cell",
"png 0.18.1",
"png",
"serde",
"thiserror 2.0.20",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4381,7 +4419,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -4651,17 +4689,6 @@ dependencies = [
"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]]
name = "objc2-osa-kit"
version = "0.3.2"
@@ -4763,7 +4790,7 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
dependencies = [
"parking_lot_core",
"parking_lot_core 0.9.12",
]
[[package]]
@@ -4786,9 +4813,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.4.4"
version = "5.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
dependencies = [
"dunce",
"is-wsl",
@@ -4858,7 +4885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4922,8 +4949,18 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
"lock_api 0.4.14",
"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]]
@@ -4934,7 +4971,19 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"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",
"windows-link 0.2.1",
]
@@ -5225,19 +5274,6 @@ dependencies = [
"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]]
name = "png"
version = "0.18.1"
@@ -5523,7 +5559,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -5667,6 +5703,15 @@ dependencies = [
"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]]
name = "redox_users"
version = "0.4.6"
@@ -5864,9 +5909,9 @@ dependencies = [
[[package]]
name = "rfd"
version = "0.16.0"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [
"block2",
"dispatch2",
@@ -5874,16 +5919,18 @@ dependencies = [
"gobject-sys 0.18.0",
"gtk-sys",
"js-sys",
"libc",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"percent-encoding",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6027,7 +6074,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6040,7 +6087,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6099,7 +6146,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -6477,7 +6524,7 @@ version = "3.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
dependencies = [
"darling",
"darling 0.23.0",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -6719,7 +6766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6737,7 +6784,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
"redox_syscall",
"redox_syscall 0.5.18",
"tracing",
"wasm-bindgen",
"web-sys",
@@ -6772,9 +6819,9 @@ dependencies = [
[[package]]
name = "specta"
version = "2.0.0-rc.25"
version = "2.0.0-rc.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
checksum = "ed155e448f42fc4be7a5cbc12bd43f6c549981f7c32fcba429ff6bc57cc4f30c"
dependencies = [
"paste",
"rustc_version 0.4.1",
@@ -6784,9 +6831,9 @@ dependencies = [
[[package]]
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"
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
checksum = "b34b2516e16a9ddd3dc4e73cd6454cfe21fe209f8cb5a6f37346fe3774d759b3"
dependencies = [
"Inflector",
"proc-macro2",
@@ -6796,9 +6843,9 @@ dependencies = [
[[package]]
name = "specta-serde"
version = "0.0.12"
version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
checksum = "4983177ed0eb28235080261953de5f9ebae8a2d107851b5852420bd2ab246245"
dependencies = [
"specta",
"specta-macros",
@@ -6806,18 +6853,18 @@ dependencies = [
[[package]]
name = "specta-typescript"
version = "0.0.12"
version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
checksum = "3ecbe75dc1d27b36141b50b02b7c73bed00945710cee6edffa86d296a159a6f9"
dependencies = [
"specta",
]
[[package]]
name = "specta-util"
version = "0.0.12"
version = "0.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
checksum = "d9bf531215ea70fba8264edecf98f12600462cf359f2038602a47aeda5ecd5b8"
dependencies = [
"specta",
]
@@ -6847,7 +6894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"parking_lot 0.12.5",
"phf_shared 0.13.1",
"precomputed-hash",
]
@@ -6983,16 +7030,15 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.39.6"
version = "0.40.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"objc2-open-directory",
"windows 0.62.2",
]
@@ -7094,7 +7140,7 @@ dependencies = [
"objc2-app-kit",
"objc2-foundation",
"objc2-ui-kit",
"parking_lot",
"parking_lot 0.12.5",
"percent-encoding",
"raw-window-handle",
"tao-macros",
@@ -7148,9 +7194,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tauri"
version = "2.12.4"
version = "2.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212"
checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072"
dependencies = [
"anyhow",
"bytes",
@@ -7203,9 +7249,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.7.3"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "641dd5fd297c99ab92717b0428cca5b9c6f960b2ac96ca131b3eebc9d70b8ddc"
checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614"
dependencies = [
"anyhow",
"cargo_toml",
@@ -7226,16 +7272,16 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.7.3"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6585c5b536c00f039d12ce70a9755172d5423bbcfd3969863baa70314c3e02f"
checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316"
dependencies = [
"base64 0.23.1",
"brotli",
"ico",
"json-patch",
"plist",
"png 0.18.1",
"png",
"proc-macro2",
"quote",
"semver 1.0.28",
@@ -7253,9 +7299,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.7.3"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e83a0cfa1bae376e8fbbdc0b1692707aed50cd6072ce85cae4789af5d6c19f"
checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -7301,7 +7347,7 @@ version = "0.1.0"
dependencies = [
"deelevate",
"libc",
"parking_lot",
"parking_lot 0.13.0",
"serde",
"specta",
"sysinfo",
@@ -7326,9 +7372,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.6.2"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b3c64e24ebcbc63aaa2d88a9c51e2507084b46620baf39ecb0baa0738a08bb4"
checksum = "ab551d9bec27bb79d1c7fe5ed38c5cfd1cebb86efa745acc65f5853f345f6e56"
dependencies = [
"dunce",
"plist",
@@ -7347,9 +7393,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-devtools"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1acb4d1b8490ee79166b72c3f8c989e066650a11aa94dfd0d71fb05c61cdbed0"
checksum = "299a62101a16c14b914ebfaf529b4b64ad9ca534a0ece1c21fc9308d1e3ba9db"
dependencies = [
"async-stream",
"bytes",
@@ -7374,9 +7420,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.8.3"
version = "2.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7461aaacf1901915ea7b9f5cd74f8224a351ed96af0a62c07fce934003cb34b"
checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5"
dependencies = [
"log",
"raw-window-handle",
@@ -7480,13 +7526,13 @@ dependencies = [
[[package]]
name = "tauri-plugin-notification"
version = "2.7.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
checksum = "5bc6c194a95a535abdfbe3bc1af9a43935b8b9b29c216b8e71af8b59132703cf"
dependencies = [
"log",
"notify-rust",
"rand 0.9.5",
"rand 0.10.2",
"serde",
"serde_json",
"serde_repr",
@@ -7520,21 +7566,11 @@ dependencies = [
"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]]
name = "tauri-plugin-shell"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8567d42b2c7264355daa1df415ec77f52aa42e885924c86a969be2e920e05521"
checksum = "7375b25f72f70f6168f27f1ba3a413e30eaa0a2098cff53a4046ebfe9e2f142d"
dependencies = [
"encoding_rs",
"libc",
@@ -7589,9 +7625,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-wdio-webdriver"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f76676d510d271d80abcf8b0552987b5b4f0eae5e3f906e9b5660d43c97c62e"
checksum = "5a775b0b59f6f12b38d1f28667311a89cd07f532f540e27d65c6eadfba1156d8"
dependencies = [
"async-trait",
"axum",
@@ -7662,9 +7698,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.12.2"
version = "2.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d61892587aea2a85245bcaec98c6fe46268874c0a0deae5535fb71ca7b93eae"
checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f"
dependencies = [
"gtk",
"http",
@@ -7672,7 +7708,6 @@ dependencies = [
"log",
"objc2",
"objc2-app-kit",
"once_cell",
"percent-encoding",
"raw-window-handle",
"softbuffer",
@@ -7689,11 +7724,12 @@ dependencies = [
[[package]]
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"
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
checksum = "fe71fc209fa4c49f959644f1ae2d70f72d5675bcb3d3ad8518122fa7b330994e"
dependencies = [
"heck 0.5.0",
"paste",
"serde",
"serde_json",
"specta",
@@ -7707,15 +7743,15 @@ dependencies = [
[[package]]
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"
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
checksum = "dbf5689a1dbf2611e2351b79ba042ca089d0a3ce45274695e79084cd732a5aec"
dependencies = [
"darling",
"darling 0.24.1",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.5",
]
[[package]]
@@ -7785,10 +7821,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -8041,9 +8077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.53.3"
version = "1.54.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
dependencies = [
"bytes",
"libc",
@@ -8399,7 +8435,7 @@ dependencies = [
"flexi_logger",
"log",
"nu-ansi-term",
"parking_lot",
"parking_lot 0.12.5",
"tokio",
"tracing",
"tracing-log",
@@ -8451,10 +8487,10 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
"png 0.18.1",
"png",
"serde",
"thiserror 2.0.20",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -8563,7 +8599,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -9009,9 +9045,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.9"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
dependencies = [
"rustls-pki-types",
]
@@ -9105,7 +9141,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -9126,7 +9162,7 @@ dependencies = [
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
"windows-version",
]
@@ -9701,7 +9737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10"
dependencies = [
"cfg-if",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -10033,9 +10069,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.1.3"
version = "9.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb"
checksum = "eaeb7f16d2605b49aef586996ded29e65642fab7259a3077e537351df6d80426"
dependencies = [
"aes",
"bzip2",
@@ -10046,6 +10082,7 @@ dependencies = [
"getrandom 0.4.3",
"hmac",
"indexmap 2.14.0",
"libc",
"lzma-rust2",
"memchr",
"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" }
tracing = "0.1.44"
tauri = { version = "2.12.4" }
tauri = { version = "2.12.5" }
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"
chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.53.3", features = [
tokio = { version = "1.54.1", features = [
"rt-multi-thread",
"macros",
"time",
"sync",
] }
flexi_logger = "0.31.10"
log = "0.4.34"
regex = "1.13.1"
log = "0.4.35"
reqwest = { version = "0.13.5", features = [
"json",
"cookies",
@@ -90,7 +89,6 @@ smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" }
serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
socket2 = "0.6.5"
# *** For Windows platform only ***
+11
View File
@@ -25,6 +25,12 @@
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复检测更新后又发布新版时,应用内更新失败
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
**🍎 macOS**
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
</details>
@@ -37,5 +43,10 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
**🍎 macOS**
- 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址
</details>
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[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 }
chrono = { workspace = true }
log = { workspace = true }
@@ -5,14 +5,14 @@ edition = "2024"
rust-version = "1.91"
[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-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light
# 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]
libc = "0.2.191"
@@ -27,9 +27,9 @@ pub struct SysInfo {
impl Default for SysInfo {
#[inline]
fn default() -> Self {
let system_name = System::name().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_name = System::name().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_arch = System::cpu_arch();
Self {
system_name,
@@ -130,9 +130,14 @@ pub fn current_gid() -> u32 {
#[inline]
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.keys().map(|name| name.to_owned()).collect()
networks
.keys()
.map(|name| name.to_string_lossy().into_owned())
.collect()
}
#[inline]
+3 -4
View File
@@ -56,7 +56,7 @@
"@tanstack/react-virtual": "^3.14.13",
"@tauri-apps/api": "2.12.4",
"@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-http": "2.8.3",
"@tauri-apps/plugin-opener": "2.7.2",
@@ -94,7 +94,7 @@
"@biomejs/biome": "^2.5.14",
"@eslint-react/eslint-plugin": "^5.20.5",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.12.3",
"@tauri-apps/cli": "2.12.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.0.0",
"@types/react": "19.3.0",
@@ -102,7 +102,7 @@
"@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.2.3",
"@vitejs/plugin-react": "^6.1.1",
"@wdio/tauri-service": "1.5.0",
"@wdio/tauri-service": "1.5.1",
"adm-zip": "^0.6.1",
"axios": "^1.20.0",
"cli-color": "^2.0.4",
@@ -117,7 +117,6 @@
"eslint-plugin-react-refresh": "^0.5.7",
"eslint-plugin-sukka": "^10.2.0",
"eslint-plugin-unused-imports": "^4.4.1",
"glob": "^14.0.0",
"globals": "^17.12.0",
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
+101 -137
View File
@@ -206,8 +206,8 @@ importers:
specifier: 2.5.0
version: 2.5.0
'@tauri-apps/plugin-dialog':
specifier: 2.8.3
version: 2.8.3
specifier: 2.8.4
version: 2.8.4
'@tauri-apps/plugin-fs':
specifier: 2.7.1
version: 2.7.1
@@ -315,8 +315,8 @@ importers:
specifier: ^10.0.1
version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))
'@tauri-apps/cli':
specifier: 2.12.3
version: 2.12.3
specifier: 2.12.4
version: 2.12.4
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -339,8 +339,8 @@ importers:
specifier: ^6.1.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':
specifier: 1.5.0
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))
specifier: 1.5.1
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:
specifier: ^0.6.1
version: 0.6.1
@@ -383,9 +383,6 @@ importers:
eslint-plugin-unused-imports:
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))
glob:
specifier: ^14.0.0
version: 14.0.3
globals:
specifier: ^17.12.0
version: 17.12.0
@@ -1384,9 +1381,6 @@ packages:
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
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':
resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
@@ -2127,87 +2121,87 @@ packages:
'@tauri-apps/api@2.12.4':
resolution: {integrity: sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==}
'@tauri-apps/cli-darwin-arm64@2.12.3':
resolution: {integrity: sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==}
'@tauri-apps/cli-darwin-arm64@2.12.4':
resolution: {integrity: sha512-OxqKSckIkoJ6vX2AjifIJpTasFC0Hbdoo7gEiCmAjauABO3U8m2MRsLWe+QTiBwDx1mIrvgA+rvI7tvDnbnHVg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.12.3':
resolution: {integrity: sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==}
'@tauri-apps/cli-darwin-x64@2.12.4':
resolution: {integrity: sha512-fz76/77wMMJOaaUN6n3CybRG0P/MTEjjqYk81mGPyhi9m6Ue5T+LWBnJP/AkPooNatBPI2vtulGqomyMJ44lCQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.3':
resolution: {integrity: sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.4':
resolution: {integrity: sha512-K0w8Bof6Lt8QKLAYhRy0yy36ZH5iA/35VuSOvnxyjvCUZ+A3JKsUTlALviH/sVgAlisaPzzmeJSH8XOn4Q2HBA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.12.3':
resolution: {integrity: sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==}
'@tauri-apps/cli-linux-arm64-gnu@2.12.4':
resolution: {integrity: sha512-j5rOnjmRUwWrm1oosf0zBYl4D+fqydgUROFCk0hvKZxvbTT8SOERes0QB7rBV/XWNy+kM6pCKzb3oSm2sRglzg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.12.3':
resolution: {integrity: sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==}
'@tauri-apps/cli-linux-arm64-musl@2.12.4':
resolution: {integrity: sha512-aOue3Fy6Jb7+oWcfAVUsVHfeTybSCt2CZoU06aO4jv4d2Ks/KnaTPwMAK3CLEduGOvBLY1lbBBrIyomqLLH5Sg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.12.3':
resolution: {integrity: sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==}
'@tauri-apps/cli-linux-riscv64-gnu@2.12.4':
resolution: {integrity: sha512-Fx6GShY/RvST+t6LMlTHAwd66+1tEaAAGrnU7cysVfaiRP1BKng5Vl6rVmO6rSLOA1Sp1dRzNYM4f0OWQGb7qA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.12.3':
resolution: {integrity: sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==}
'@tauri-apps/cli-linux-x64-gnu@2.12.4':
resolution: {integrity: sha512-tNzZSa79Z1nRpHxEpQ38lsAFpKlFlYvoXzjTV+lRRn/zSf3wrdUC3wnLcq2XEadchPu4ttK0zivhYzr+XGD19Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.12.3':
resolution: {integrity: sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==}
'@tauri-apps/cli-linux-x64-musl@2.12.4':
resolution: {integrity: sha512-jmZ1pr0xEqUhuEusWtt9Yt+GF6lCnGHUmIL+KIknpbjK/XQ2qkhDBhaxK/JMHM2A8dAacVXCxXJk2EXpwhQUfA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.12.3':
resolution: {integrity: sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==}
'@tauri-apps/cli-win32-arm64-msvc@2.12.4':
resolution: {integrity: sha512-fGo8pgEHZxdSYgZ/J98m1usJ7NHKmKbq7k6uT0x1rpOq6Bg2YVzzpaAblWk710f5LHYBJIL5QIFS5IrD+4gmTg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.12.3':
resolution: {integrity: sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==}
'@tauri-apps/cli-win32-ia32-msvc@2.12.4':
resolution: {integrity: sha512-ezl8sQIcjYsye3TUnTH5VcWmWzccZASlQm7hlZt0dYyj1XD0qpVW47u2xYVrIKui0F8uQv/7QGZixdWiClBsbg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.12.3':
resolution: {integrity: sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==}
'@tauri-apps/cli-win32-x64-msvc@2.12.4':
resolution: {integrity: sha512-DJhDeCqjLDNMWYFy7ZS8cfeAHX2RK0RbQMLiA3bMVVkXRDJ4NBYiHriGqsUhsr9iG41aTb+dJGkFPmPw1gWwVw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.12.3':
resolution: {integrity: sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==}
'@tauri-apps/cli@2.12.4':
resolution: {integrity: sha512-JI9bHfudSRJikGOrjbXEm6RjzzvmKuLc+JixB1NE8hNzQkNlCCIC3pTES3uypkR9lxHrsbxzcnSwhI2IxBQphw==}
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.5.0':
resolution: {integrity: sha512-dibUy1nKURYDXRMvjNk4deAbTZ5jPQ8wCE4nolt5xniryWgXQ4sK2GkFXoXlHbRdPzfZptZj95BRLfalA7m/2Q==}
'@tauri-apps/plugin-dialog@2.8.3':
resolution: {integrity: sha512-Hd/yBfk2R5v2MFrz/0VArSXeh8xKLLXGunqSiTtcHKn1zNZwaKujuA0VSwUvNfwC4hDoF1PveS+gBDNe3nHspw==}
'@tauri-apps/plugin-dialog@2.8.4':
resolution: {integrity: sha512-+6ZOiXenOaxXwb8qCbBPdT2jX2cFPaVVHGIskrpmJLpmWj+qKRTD6X/z9x7hTdSQn7NedcoXCkWGej4a7dC4lw==}
'@tauri-apps/plugin-fs@2.7.1':
resolution: {integrity: sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==}
@@ -2623,20 +2617,20 @@ packages:
resolution: {integrity: sha512-0ZAEIo6PNyMIJPlOGkIgyOJUjcd0pC8/QHlVAAe1c91/IcjZ1X+k0yidXHaboJdN7dq1XPUacmhRdtua0U5EZg==}
engines: {node: '>=18.20.0'}
'@wdio/native-core@1.3.0':
resolution: {integrity: sha512-wUwc0QTdvhD0LwH4YcRaN9JWLX/yac8sovddUR5/4sqHMNJr3yaN11bpIfYoD/bEk+1nTLjWuDCFpe5l5wUgLQ==}
'@wdio/native-core@1.3.1':
resolution: {integrity: sha512-fdGk2lEr2vS+bClJXuZEnsG30cLzQv+B9W+dAt/Yte/fE33s/vDBn7hHwFx8aZLTOS0pPExErUxYiZEMXX+04g==}
engines: {node: '>=18.20.0'}
'@wdio/native-spy@1.4.0':
resolution: {integrity: sha512-unsC2Vgu5T9atD478Zj/p2N+rVSK4i/NibIaaEw4rZF/T5u5bmAHChFntEiBEfyMzd62VwbhKhzwwOSKjzrfJA==}
'@wdio/native-spy@1.4.1':
resolution: {integrity: sha512-KGOmgaicHqDkqj+s2bxXsRsb27IzW6carlyTu1xcrJI34yUbC0v14F8mbhuZcewW+mt2xhmC17yITwAbNa8PKw==}
engines: {node: '>=18.20.0'}
'@wdio/native-types@2.6.0':
resolution: {integrity: sha512-MVMVpZTH+UY6nUtLClCzY4BQSIs/nCCHo81i4ClLR+b2NehyBvwccttXDAGPgf0icyHUhd+PgIPXaLgRcm4M9A==}
'@wdio/native-types@2.6.1':
resolution: {integrity: sha512-Rm5chX+MrRhVgJYcYdcX/yyTZaLjxvi0w/fnSgPaDT16gMIntXlk22K2x3hwEdaUonOKepZyhh4jr9ZgRz7UaQ==}
engines: {node: '>=18.20.0'}
'@wdio/native-utils@2.8.0':
resolution: {integrity: sha512-TmIkPAd19FkJ747AdFVEtawbakVV3HHCh8zk3Kl/FLc6B2bbea7hn90jtLxqBZgCVqUuJIBcMNFg/nV/hgBFOw==}
'@wdio/native-utils@2.8.1':
resolution: {integrity: sha512-QbmZjGkLUC2bmUJSzyqsHM11HND1D7tkxHFssUV1Kmx43+oPAeHHVR0NSZu2hMzSNrDZMBAmNYycR+3X8rqWcA==}
engines: {node: '>=18.20.0'}
peerDependencies:
tsx: ^4.22.4
@@ -2660,8 +2654,8 @@ packages:
resolution: {integrity: sha512-lNLLPUk4y1Yp5pWwBFF+tjZIy7bOv0TPTk4HDWV4LnnHnkc24abI0nN+kb2y5/JV8ZgwLCCVKUFRdRUxfKsAaQ==}
engines: {node: '>=18.20.0'}
'@wdio/tauri-service@1.5.0':
resolution: {integrity: sha512-S3fs8T+jgNOFi4x/X2z69unS/1K3ZB28+owBouhHtVnrMKeBWAKP46IHnp8c8AZXppn6fiPNC4M1jfRZbLKeKw==}
'@wdio/tauri-service@1.5.1':
resolution: {integrity: sha512-nfzJU2x7bxvIXtYo+yvrX4Xwuba8h5hXOlJ0IyHanGg0ELavY/2UfyxygJDTcSGO+pCQMdC9JJEyMvaS50AtZQ==}
engines: {node: '>=18.20.0'}
peerDependencies:
webdriverio: ^9.0.0
@@ -3508,8 +3502,8 @@ packages:
resolution: {integrity: sha512-dUnV6Uok578HuGYTC/EM0LqeXewXBCHsh/kmLkdcqN1zgSy7K3gCIrshPBS5JwW20XSaRv4Vnz7z8lMjOV8jWw==}
engines: {node: '>=18.0.0'}
follow-redirects@1.16.0:
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
follow-redirects@1.16.2:
resolution: {integrity: sha512-/kVP2U8z596Ia0gwFrwyIeo74ta5aVmSeU1FfAUMAmcEP5EP7zydmag3SLiN64NaRKef3lxsq7i/g7ltJQDGow==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
@@ -3590,10 +3584,6 @@ packages:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
glob@14.0.3:
resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==}
engines: {node: 18 || 20 || >=22}
globals@17.12.0:
resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
engines: {node: '>=18'}
@@ -4131,10 +4121,6 @@ packages:
resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==}
engines: {node: 18 || 20 || >=22}
minimatch@11.0.2:
resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==}
engines: {node: 18 || 20 || >=22}
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -4296,10 +4282,6 @@ packages:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
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:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
@@ -4602,8 +4584,8 @@ packages:
snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
@@ -4825,16 +4807,16 @@ packages:
undici-types@8.9.0:
resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
undici@6.28.1:
resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==}
undici@6.29.1:
resolution: {integrity: sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==}
engines: {node: '>=18.17'}
undici@7.29.1:
resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==}
undici@7.30.1:
resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==}
engines: {node: '>=20.18.1'}
undici@8.11.2:
resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==}
undici@8.12.3:
resolution: {integrity: sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==}
engines: {node: '>=22.19.0'}
unicode-canonical-property-names-ecmascript@2.0.1:
@@ -5156,12 +5138,12 @@ snapshots:
'@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8)
'@octokit/request': 10.0.16
'@octokit/request-error': 7.1.2
undici: 6.28.1
undici: 6.29.1
'@actions/http-client@3.0.2':
dependencies:
tunnel: 0.0.6
undici: 6.28.1
undici: 6.29.1
'@babel/code-frame@7.29.7':
dependencies:
@@ -6260,8 +6242,6 @@ snapshots:
dependencies:
minipass: 7.1.3
'@isaacs/nonrecursive-glob-match@1.0.2': {}
'@jest/diff-sequences@30.5.0': {}
'@jest/expect-utils@30.5.2':
@@ -6824,58 +6804,58 @@ snapshots:
'@tauri-apps/api@2.12.4': {}
'@tauri-apps/cli-darwin-arm64@2.12.3':
'@tauri-apps/cli-darwin-arm64@2.12.4':
optional: true
'@tauri-apps/cli-darwin-x64@2.12.3':
'@tauri-apps/cli-darwin-x64@2.12.4':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.3':
'@tauri-apps/cli-linux-arm-gnueabihf@2.12.4':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.12.3':
'@tauri-apps/cli-linux-arm64-gnu@2.12.4':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.12.3':
'@tauri-apps/cli-linux-arm64-musl@2.12.4':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.12.3':
'@tauri-apps/cli-linux-riscv64-gnu@2.12.4':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.12.3':
'@tauri-apps/cli-linux-x64-gnu@2.12.4':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.12.3':
'@tauri-apps/cli-linux-x64-musl@2.12.4':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.12.3':
'@tauri-apps/cli-win32-arm64-msvc@2.12.4':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.12.3':
'@tauri-apps/cli-win32-ia32-msvc@2.12.4':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.12.3':
'@tauri-apps/cli-win32-x64-msvc@2.12.4':
optional: true
'@tauri-apps/cli@2.12.3':
'@tauri-apps/cli@2.12.4':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.12.3
'@tauri-apps/cli-darwin-x64': 2.12.3
'@tauri-apps/cli-linux-arm-gnueabihf': 2.12.3
'@tauri-apps/cli-linux-arm64-gnu': 2.12.3
'@tauri-apps/cli-linux-arm64-musl': 2.12.3
'@tauri-apps/cli-linux-riscv64-gnu': 2.12.3
'@tauri-apps/cli-linux-x64-gnu': 2.12.3
'@tauri-apps/cli-linux-x64-musl': 2.12.3
'@tauri-apps/cli-win32-arm64-msvc': 2.12.3
'@tauri-apps/cli-win32-ia32-msvc': 2.12.3
'@tauri-apps/cli-win32-x64-msvc': 2.12.3
'@tauri-apps/cli-darwin-arm64': 2.12.4
'@tauri-apps/cli-darwin-x64': 2.12.4
'@tauri-apps/cli-linux-arm-gnueabihf': 2.12.4
'@tauri-apps/cli-linux-arm64-gnu': 2.12.4
'@tauri-apps/cli-linux-arm64-musl': 2.12.4
'@tauri-apps/cli-linux-riscv64-gnu': 2.12.4
'@tauri-apps/cli-linux-x64-gnu': 2.12.4
'@tauri-apps/cli-linux-x64-musl': 2.12.4
'@tauri-apps/cli-win32-arm64-msvc': 2.12.4
'@tauri-apps/cli-win32-ia32-msvc': 2.12.4
'@tauri-apps/cli-win32-x64-msvc': 2.12.4
'@tauri-apps/plugin-clipboard-manager@2.5.0':
dependencies:
'@tauri-apps/api': 2.12.4
'@tauri-apps/plugin-dialog@2.8.3':
'@tauri-apps/plugin-dialog@2.8.4':
dependencies:
'@tauri-apps/api': 2.12.4
@@ -7275,11 +7255,11 @@ snapshots:
safe-regex2: 5.1.1
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:
'@wdio/logger': 9.29.1
'@wdio/native-types': 2.6.0
'@wdio/native-utils': 2.8.0(supports-color@7.2.0)
'@wdio/native-types': 2.6.1
'@wdio/native-utils': 2.8.1(supports-color@7.2.0)
debug: 4.4.3(supports-color@7.2.0)
get-port: 7.2.0
tslib: 2.8.1
@@ -7287,11 +7267,11 @@ snapshots:
- supports-color
- 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:
'@wdio/logger': 9.29.1
debug: 4.4.3(supports-color@7.2.0)
@@ -7325,14 +7305,14 @@ snapshots:
easy-table: 1.2.0
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:
'@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/native-core': 1.3.0(supports-color@7.2.0)
'@wdio/native-spy': 1.4.0
'@wdio/native-types': 2.6.0
'@wdio/native-utils': 2.8.0(supports-color@7.2.0)
'@wdio/native-core': 1.3.1(supports-color@7.2.0)
'@wdio/native-spy': 1.4.1
'@wdio/native-types': 2.6.1
'@wdio/native-utils': 2.8.1(supports-color@7.2.0)
'@wdio/spec-reporter': 9.31.2
'@wdio/types': 9.31.2
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):
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
https-proxy-agent: 5.0.1(supports-color@7.2.0)
proxy-from-env: 2.1.0
@@ -7628,7 +7608,7 @@ snapshots:
parse5: 7.3.0
parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2
undici: 7.29.1
undici: 7.30.1
whatwg-mimetype: 4.0.0
chokidar@5.0.0:
@@ -7850,7 +7830,7 @@ snapshots:
'@wdio/logger': 10.1.0
'@zip.js/zip.js': 2.24.0
decamelize: 6.0.1
undici: 8.11.2
undici: 8.12.3
electron-to-chromium@1.5.444: {}
@@ -8309,7 +8289,7 @@ snapshots:
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:
debug: 4.4.3(supports-color@7.2.0)
@@ -8351,7 +8331,7 @@ snapshots:
'@zip.js/zip.js': 2.24.0
decamelize: 6.0.1
modern-tar: 0.8.5
undici: 8.11.2
undici: 8.12.3
gensync@1.0.0-beta.2: {}
@@ -8393,12 +8373,6 @@ snapshots:
minipass: 7.1.3
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: {}
gopd@1.2.0: {}
@@ -9089,11 +9063,6 @@ snapshots:
dependencies:
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: {}
minizlib@3.1.0:
@@ -9294,11 +9263,6 @@ snapshots:
lru-cache: 11.5.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: {}
pathe@2.0.3: {}
@@ -9314,7 +9278,7 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
source-map-js: 1.2.1
source-map-js: 1.2.2
prelude-ls@1.2.1: {}
@@ -9561,7 +9525,7 @@ snapshots:
dependencies:
chokidar: 5.0.0
immutable: 5.1.9
source-map-js: 1.2.1
source-map-js: 1.2.2
optionalDependencies:
'@parcel/watcher': 2.6.0
@@ -9599,7 +9563,7 @@ snapshots:
dot-case: 3.0.4
tslib: 2.8.1
source-map-js@1.2.1: {}
source-map-js@1.2.2: {}
source-map-support@0.5.21:
dependencies:
@@ -9817,11 +9781,11 @@ snapshots:
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: {}
@@ -10022,7 +9986,7 @@ snapshots:
'@wdio/utils': 10.1.0(supports-color@7.2.0)(yauzl@2.10.0)
deepmerge-ts: 8.0.2
https-proxy-agent: 9.1.0(supports-color@7.2.0)
undici: 8.11.2
undici: 8.12.3
ws: 8.22.1
transitivePeerDependencies:
- bufferutil
+7 -21
View File
@@ -116,37 +116,23 @@
},
{
"description": "Update the Specta contract toolchain together",
"matchManagers": [
"cargo"
],
"matchPackageNames": [
"specta",
"tauri-specta",
"specta-typescript"
],
"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"
],
"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"
],
"matchManagers": ["cargo"],
"matchPackageNames": ["specta-typescript"],
"allowedVersions": "/^0\\.0\\.13$/"
}
],
+1 -2
View File
@@ -6,7 +6,6 @@ import path from 'node:path'
import zlib from 'node:zlib'
import AdmZip from 'adm-zip'
import { glob } from 'glob'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { extract } from 'tar'
@@ -519,7 +518,7 @@ const resolveServicePermission = async () => {
let hasChanges = false
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) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath)
+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.
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.
@@ -50,31 +52,62 @@ function dns_of() {
echo "$dns"
}
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
[ $# -lt 1 ] && echo "Usage: $0 <newline-separated DNS servers> [legacy state directory]" >&2 && 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.
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName)
[ -z "$hardware_port" ] && echo "No primary network service." && exit 1
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService) || exit 1
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName) || 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; }
backup=".original_dns/$service_id"
mkdir -p .original_dns || exit 1
# Older releases kept a single backup for whichever service was primary.
if [ -f .original_dns.txt ]; then
if [ -f "$backup" ]; then
rm -f .original_dns.txt || exit 1
else
mv .original_dns.txt "$backup" || exit 1
# Key old single-slot backups before import so a later network switch cannot reassign them.
for state_dir in "$PWD" "${2:-$PWD}"; do
if [ -f "$state_dir/.original_dns.txt" ]; then
mkdir -p "$state_dir/.original_dns" || exit 1
if [ -f "$state_dir/.original_dns/$service_id" ]; then
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
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
# The placeholder is ours, not the user's: keep the backup taken before it was applied.
if [ "$original_dns" != "$1" ]; then
echo "$original_dns" >"$backup" || exit 1
networksetup -setdnsservers "$hardware_port" "$1"
# Only an unmarked historical backup owns the old single-address placeholder.
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; }
elif [ ! -f "$backup" ]; then
echo "empty" >"$backup" || exit 1
fi
# 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
+64 -35
View File
@@ -1,9 +1,11 @@
#!/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.
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.
@@ -21,42 +23,69 @@ function dns_of() {
echo "$dns"
}
result=0
# Older releases kept a single backup for whichever service was primary.
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
backup_header='# clash-verge-dns-managed'
completed_backups=()
for backup in .original_dns/*; do
[ -f "$backup" ] || continue
hardware_port=$(sc_value "Setup:/Network/Service/${backup##*/}" UserDefinedName)
# 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
continue
function restore_dns() {
local result=0
# Older releases kept a single backup for whichever service was primary.
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
# A service no longer on the placeholder was changed by someone else, and that change stands.
if [ "$current_dns" = "$1" ]; then
original_dns=$(cat "$backup") || { result=1; continue; }
# Older releases could back up the placeholder itself.
[ "$original_dns" = "$1" ] && original_dns=empty
networksetup -setdnsservers "$hardware_port" $original_dns
if [ "$(dns_of "$hardware_port")" != "$original_dns" ]; then
echo "Failed to restore DNS for $hardware_port." >&2
for backup in .original_dns/*; do
[ -f "$backup" ] || continue
# Current backups take precedence even after their DNS has already been restored.
if [ "$PWD" != "$state_dir" ] && [ -f "$state_dir/$backup" ]; then
completed_backups+=("$PWD/$backup")
continue
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
continue
fi
fi
rm -f "$backup" || result=1
done
rmdir .original_dns 2>/dev/null
original_dns=$(cat "$backup") || { result=1; continue; }
legacy_backup=false
case "$original_dns" in
"$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
+15 -20
View File
@@ -29,7 +29,7 @@ perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.7.3", features = [] }
tauri-build = { version = "2.7.4", features = [] }
[dependencies]
clash-verge-draft = { workspace = true }
@@ -55,11 +55,10 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml_ng = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
open = "5.4.4"
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
open = "5.4.5"
rfd = { version = "0.17.2", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0"
dunce = "1.0.5"
nanoid = "0.5"
@@ -67,19 +66,17 @@ chrono = { workspace = true }
boa_engine = "0.22.0"
percent-encoding = "2.3.2"
reqwest = { workspace = true }
regex = { workspace = true }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch = "main", features = [
"guard",
] }
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-dialog = "2.8.3"
tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1"
tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.2"
tauri-plugin-deep-link = "2.6.3"
tauri-plugin-window-state = "2.5.0"
zip = "9.1.3"
zip = "9.2.2"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
@@ -89,27 +86,26 @@ getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.7.0"
tokio-stream = "0.1.19"
tauri-plugin-notification = "2.8.0"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3"
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" }
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 = [
"client",
] }
arc-swap = "1.9.2"
arc-swap = "1.9.3"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.9"
webpki-roots = "1.0.11"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] }
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
specta-typescript = "=0.0.12"
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.26", features = ["derive", "typescript"] }
specta-typescript = "=0.0.13"
[target.'cfg(unix)'.dependencies]
libc = "0.2.191"
@@ -119,7 +115,7 @@ libc = "0.2.191"
clash-verge-signal = { workspace = true }
[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-foundation = { version = "0.3.2", features = [
"NSString",
@@ -156,7 +152,6 @@ tauri-plugin-global-shortcut = "2.4.1"
tauri-plugin-updater = "2.14.1"
[dev-dependencies]
criterion = { workspace = true }
tokio = { workspace = true, features = ["test-util"] }
warp = { version = "0.4.3", features = ["test"] }
-1
View File
@@ -73,7 +73,6 @@
"global-shortcut:allow-register-all",
"global-shortcut:allow-unregister",
"global-shortcut:allow-unregister-all",
"process:allow-exit",
"clipboard-manager:allow-read-text",
"clipboard-manager:allow-write-text",
"dialog:default"
+53
View File
@@ -614,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}]"))
}
// 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(
activations: &[(String, String)],
completed: &HashMap<String, String>,
@@ -1019,6 +1066,12 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
.clone()
.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 is_activation_current(generation) {
announce(Refresh::Clash);
+18 -6
View File
@@ -559,12 +559,13 @@ impl CoreManager {
})?;
#[cfg(target_os = "macos")]
crate::utils::resolve::dns::sync_public_dns().await;
// At login the app usually beats the network. Leave the write to the network watcher
// rather than fail — only if the watcher is actually live; a user toggling while
// offline still fails fast.
// Only automatic restores can wait for the watcher; explicit requests must report failure.
#[cfg(target_os = "macos")]
if crate::core::network_watch::is_armed()
&& !crate::utils::resolve::is_resolve_done()
let defer_until_network = crate::core::network_watch::is_armed()
&& crate::core::notification::what_was_asked()
== crate::core::notification::FailedOperation::SystemProxyRestore;
#[cfg(target_os = "macos")]
if defer_until_network
&& Config::verge()
.await
.latest_arc()
@@ -579,7 +580,18 @@ impl CoreManager {
);
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(
self.get_running_mode().as_ref(),
self.current_core_readiness_generation(),
+1
View File
@@ -49,6 +49,7 @@ async fn reapply() {
.enable_system_proxy
.unwrap_or_default()
{
crate::utils::resolve::dns::sync_public_dns().await;
return;
}
if !proxy_control::has_network_service().await {
+24 -4
View File
@@ -180,6 +180,14 @@ pub const fn is_missing_network_service(_error: &sysproxy::Error) -> bool {
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.
async fn classify_local_apply_failure(error: anyhow::Error) -> anyhow::Error {
if SysproxyFailure::from_chain(&error).is_some() || !was_refused_locally(&error) {
@@ -359,6 +367,13 @@ fn service_apply_result(requested: &MacosProxyConfig, outcome: ProxyApplyOutcome
ProxyApplyOutcome::Applied | ProxyApplyOutcome::NotRequested => Ok(()),
// Direct fallback satisfies a disable request.
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 } => {
Err(SysproxyFailure::DirectFallback { detail: message }.into())
}
@@ -609,6 +624,10 @@ pub async fn refresh_guard() -> Result<()> {
logging!(debug, Type::Core, "proxy guard superseded (generation {generation})");
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) => {
consecutive_failures += 1;
logging!(
@@ -1110,10 +1129,11 @@ mod tests {
.err()
.unwrap_or_else(|| anyhow::anyhow!("an enable that never reached the OS must fail"));
assert_eq!(
SysproxyFailure::from_chain(&error).map(SysproxyFailure::code),
Some("SYSPROXY_DIRECT_FALLBACK")
);
assert!(super::is_missing_network_service_failure(&error));
assert!(SysproxyFailure::from_chain(&error).is_none());
assert!(!super::is_missing_network_service_failure(
&error.context(SystemProxyStateUnknown)
));
}
#[cfg(target_os = "macos")]
+1 -1
View File
@@ -1970,7 +1970,7 @@ impl ServiceManager {
async fn run_operation(&self, operation: impl Future<Output = Result<()>>) -> Result<()> {
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!(
warn,
Type::Service,
+2 -2
View File
@@ -7,6 +7,7 @@ use crate::{
};
use anyhow::Result;
use clash_verge_logging::{Type, logging, logging_error};
use futures::StreamExt as _;
use parking_lot::{Mutex, RwLock};
use smartstring::alias::String;
use std::{
@@ -21,7 +22,6 @@ use tokio::{
sync::mpsc,
time::{sleep, timeout},
};
use tokio_stream::StreamExt as _;
use tokio_util::time::{DelayQueue, delay_queue::Key};
enum TimerCommand {
@@ -501,6 +501,7 @@ impl Timer {
mod tests {
use super::{TaskSchedule, Timer, run_timer_profile_update_transition};
use crate::config::{PrfItem, PrfOption};
use futures::StreamExt as _;
use parking_lot::Mutex;
use smartstring::alias::String;
use std::collections::HashMap;
@@ -513,7 +514,6 @@ mod tests {
time::Duration,
};
use tokio::sync::Barrier;
use tokio_stream::StreamExt as _;
use tokio_util::time::DelayQueue;
struct CancellationProbe {
+6
View File
@@ -66,6 +66,12 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> {
return Err(err.to_string().into());
}
if mode.as_str() == "global"
&& let Err(err) = crate::config::profiles::leave_default_global_exit().await
{
logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}");
}
let clash = Config::clash().await;
let save_result = {
let _config_write = Config::lock_config_write().await;
-1
View File
@@ -54,7 +54,6 @@ mod app_init {
.build(),
)
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
+24 -54
View File
@@ -4,16 +4,14 @@ use serde_yaml_ng::{Mapping, Value};
use std::{
io::Read as _,
os::unix::process::CommandExt as _,
path::{Path, PathBuf},
path::PathBuf,
process::{Command, Stdio},
time::{Duration, Instant},
};
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.
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.
pub(crate) const DNS_SCRIPT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -26,14 +24,6 @@ fn dns_state_dir() -> anyhow::Result<PathBuf> {
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.
fn needs_public_dns(config: &Mapping) -> bool {
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() {
logging!(debug, Type::Config, "try to set system dns");
let result = async {
let script = crate::utils::dirs::app_resources_dir()?.join("set_dns.sh");
run_dns_script(&script, dns_state_dir()?).await
}
.await;
let result = run_dns_script("set_dns.sh", true).await;
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:#}"),
}
}
async fn unset_public_dns() -> anyhow::Result<()> {
logging!(debug, Type::Config, "try to unset system dns");
let result = async {
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;
let result = run_dns_script("unset_dns.sh", false).await.map(|_| ());
match &result {
Ok(()) => logging!(info, Type::Config, "unset system dns successfully"),
Err(err) => logging!(error, Type::Config, "unset system dns failed: {err:#}"),
@@ -105,12 +89,21 @@ async fn unset_public_dns() -> anyhow::Result<()> {
result
}
enum DnsScriptOutcome {
Complete,
Deferred,
}
/// 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());
let mut child = Command::new("bash")
.arg(script)
.arg(&script)
.arg(PLACEHOLDER_DNS)
.arg(resource_dir)
.current_dir(dir)
.stdout(Stdio::null())
.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() {
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!(
status.success(),
"exit code {}: {}",
status.code().unwrap_or(-1),
stderr.trim()
);
Ok(())
Ok(DnsScriptOutcome::Complete)
})
.await
.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");
}
}
+2 -2
View File
@@ -33,9 +33,9 @@
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK",
"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://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"
"https://gh-proxy.org/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json"
],
"windows": {
"installMode": "passive"
+2 -1
View File
@@ -26,8 +26,9 @@
"active": true,
"dialog": false,
"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://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"
}
+2 -1
View File
@@ -26,8 +26,9 @@
"active": true,
"dialog": false,
"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://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"
}
+2 -1
View File
@@ -26,8 +26,9 @@
"active": true,
"dialog": false,
"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://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"
}
@@ -464,21 +464,6 @@ export const ConnectionTable = (props: Props) => {
]
}, [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 baseFields = baseColumns.map((column) => column.field)
const reconciledOrder = reconcileColumnOrder(columnOrder, baseFields)
+64 -73
View File
@@ -1,4 +1,3 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import {
AccessTimeRounded,
ArrowDropDown,
@@ -31,7 +30,15 @@ import {
useTheme,
} from '@mui/material'
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 { useNavigate } from 'react-router'
@@ -570,20 +577,22 @@ export const CurrentProxyCard = () => {
return profileValue
}
if (profileKey !== baseKey) {
const legacyValue = localStorage.getItem(baseKey)
if (legacyValue != null) {
localStorage.removeItem(baseKey)
localStorage.setItem(profileKey, legacyValue)
return legacyValue
}
}
return null
return profileKey !== baseKey ? localStorage.getItem(baseKey) : null
},
[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(
(baseKey: string, value: string) => {
if (typeof window === 'undefined') return
@@ -615,25 +624,17 @@ export const CurrentProxyCard = () => {
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0
})
const [selectedGroupName, setSelectedGroupName] = useState('')
const [lastGroupName, setLastGroupName] = useState('')
const [openSelect, setOpenSelect] = useState<OpenSelect>(null)
useEffect(() => {
const [openSelectMode, setOpenSelectMode] = useState(mode)
if (openSelectMode !== mode) {
setOpenSelectMode(mode)
setOpenSelect(null)
}, [mode])
}
const delayButtonRef = useRef<HTMLButtonElement>(null)
const delays = useGroupDelays(selectedGroupName || null)
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(() => {
if (!proxyView) return []
@@ -644,6 +645,39 @@ export const CurrentProxyCard = () => {
)
}, [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>(() => {
if (!proxyView || isDirectMode) return null
if (isGlobalMode) return proxyView.global
@@ -680,46 +714,6 @@ export const CurrentProxyCard = () => {
[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(() => {
if (!proxyView) return undefined
if (isDirectMode) {
@@ -743,12 +737,10 @@ export const CurrentProxyCard = () => {
: undefined
}, [isDirectMode, proxyView, selectedGroup])
latestProxyMemberRef.current = currentOption?.member ?? null
const handleGroupChange = useCallback(
(newGroupName: string) => {
if (isGlobalMode || isDirectMode) return
setSelectedGroupName(newGroupName)
setLastGroupName(newGroupName)
writeProfileScopedItem(STORAGE_KEY_GROUP, newGroupName)
},
[isDirectMode, isGlobalMode, writeProfileScopedItem],
@@ -793,7 +785,7 @@ export const CurrentProxyCard = () => {
color: 'text.secondary',
}
const checkCurrentProxyDelay = useCallback(async () => {
const checkCurrentProxyDelay = useEffectEvent(async () => {
if (autoCheckInProgressRef.current) return
if (isDirectMode) return
@@ -802,7 +794,7 @@ export const CurrentProxyCard = () => {
if (!groupName || !proxyName) return
const proxyMember = latestProxyMemberRef.current
const proxyMember = currentOption?.member
if (!proxyMember || !isInteractableMember(proxyMember)) {
debugLog(
`[CurrentProxyCard] 自动延迟检测跳过,组: ${groupName}, 节点: ${proxyName} 未找到`,
@@ -812,7 +804,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = true
const timeout = latestTimeoutRef.current || 10000
const timeout = verge?.default_latency_timeout || 10000
try {
debugLog(
@@ -828,7 +820,7 @@ export const CurrentProxyCard = () => {
autoCheckInProgressRef.current = false
refreshProxy()
}
}, [isDirectMode, refreshProxy, selectedGroupName, selectedProxyName])
})
useEffect(() => {
if (isDirectMode) return
@@ -858,7 +850,6 @@ export const CurrentProxyCard = () => {
if (intervalTimer) clearTimeout(intervalTimer)
}
}, [
checkCurrentProxyDelay,
autoDelayIntervalMs,
isDirectMode,
selectedGroupName,
@@ -4,6 +4,7 @@ import {
memo,
useCallback,
useEffect,
useEffectEvent,
useImperativeHandle,
useMemo,
useReducer,
@@ -120,7 +121,6 @@ export const EnhancedCanvasTrafficGraph = memo(
const drawFrameRef = useRef<number | undefined>(undefined)
const hoverFrameRef = useRef<number | undefined>(undefined)
const mouseMoveFrameRef = useRef<number | undefined>(undefined)
const scheduleDrawGraphRef = useRef<() => void>(() => {})
const pendingMousePositionRef = useRef<{
clientX: number
clientY: number
@@ -189,11 +189,11 @@ export const EnhancedCanvasTrafficGraph = memo(
}
}, [displayData])
const handleFocusStateChange = useCallback((focused: boolean) => {
const handleFocusStateChange = useEffectEvent((focused: boolean) => {
isWindowFocusedRef.current = focused
scheduleDrawGraphRef.current()
}, [])
scheduleDrawGraph()
})
useEffect(() => {
if (typeof window === 'undefined' || typeof document === 'undefined') {
@@ -217,7 +217,7 @@ export const EnhancedCanvasTrafficGraph = memo(
window.removeEventListener('blur', handleBlur)
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [handleFocusStateChange])
}, [])
const calculateY = useCallback(
(
@@ -906,25 +906,21 @@ export const EnhancedCanvasTrafficGraph = memo(
scheduleDrawGraph()
}, [scheduleDrawGraph])
useEffect(() => {
scheduleDrawGraphRef.current = scheduleDrawGraph
}, [scheduleDrawGraph])
const redrawGraph = useEffectEvent(() => scheduleDrawGraph())
useEffect(() => {
const canvas = canvasRef.current
if (!canvas || typeof window === 'undefined') return
if (typeof ResizeObserver === 'undefined') {
const handleResize = () => scheduleDrawGraphRef.current()
const handleResize = () => redrawGraph()
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}
const resizeObserver = new ResizeObserver(() =>
scheduleDrawGraphRef.current(),
)
const resizeObserver = new ResizeObserver(() => redrawGraph())
resizeObserver.observe(canvas)
return () => {
+18 -20
View File
@@ -8,7 +8,7 @@ import {
} from '@mui/icons-material'
import { Typography, Stack, Divider, Chip, IconButton } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
@@ -22,10 +22,23 @@ import {
import { useVerge } from '@/hooks/use-verge'
import { getSystemInfo } from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { version as appVersion } from '@root/package.json'
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 = () => {
const { t } = useTranslation()
const { verge, patchVerge } = useVerge()
@@ -36,31 +49,16 @@ export const SystemInfoCard = () => {
const { checkUpdate: triggerCheckUpdate, lastCheckUpdate } = useUpdate(true)
const [osInfo, setOsInfo] = useState('')
const { data: osInfo = '' } = useQuery({
queryKey: ['getSystemInfo'],
queryFn: getOsInfo,
})
const lastCheckUpdateText = useMemo(
() => (lastCheckUpdate ? new Date(lastCheckUpdate).toLocaleString() : '-'),
[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(() => {
if (!verge?.auto_check_update) return
if (readLastCheckTime() !== null) return
+5 -7
View File
@@ -21,6 +21,7 @@ import {
memo,
useCallback,
useEffect,
useEffectEvent,
useReducer,
useRef,
useState,
@@ -113,7 +114,6 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const setProfileLoading = useSetProfileLoading()
const [showNextUpdate, setShowNextUpdate] = useState(false)
const showNextUpdateRef = useRef(false)
const [nextUpdateTime, setNextUpdateTime] = useState('')
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
@@ -203,10 +203,6 @@ const ProfileItemBase = (props: ProfileItemProps) => {
setShowNextUpdate(!showNextUpdate)
}
useEffect(() => {
showNextUpdateRef.current = showNextUpdate
}, [showNextUpdate])
useEffect(() => {
if (showNextUpdate) {
fetchNextUpdateTime()
@@ -218,8 +214,10 @@ const ProfileItemBase = (props: ProfileItemProps) => {
updated,
])
const isNextUpdateShown = useEffectEvent(() => showNextUpdate)
useEffect(() => {
if (timerUpdateRevision === 0 || !showNextUpdateRef.current) return
if (timerUpdateRevision === 0 || !isNextUpdateShown()) return
if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current)
@@ -236,7 +234,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
}, [fetchNextUpdateTime, timerUpdateRevision])
useEffect(() => {
if (completedUpdateRevision === 0 || !showNextUpdateRef.current) return
if (completedUpdateRevision === 0 || !isNextUpdateShown()) return
fetchNextUpdateTime()
}, [completedUpdateRevision, fetchNextUpdateTime])
+19 -14
View File
@@ -10,7 +10,7 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
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 { 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(
handleSubmit(async (form) => {
setLoading(true)
@@ -419,7 +408,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.useSystemProxy')}
</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>
)}
/>
@@ -432,7 +429,15 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.useClashProxy')}
</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>
)}
/>
+30
View File
@@ -1,3 +1,5 @@
import * as yaml from 'js-yaml'
import {
getRecord,
rebindNode,
@@ -34,3 +36,31 @@ export const rebindProxyChainItems = (
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,
useTheme,
} from '@mui/material'
import * as yaml from 'js-yaml'
import {
type Ref,
useCallback,
@@ -60,18 +59,9 @@ const chainPointerSensor = PointerSensor.configure({
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
interface ParsedChainConfig {
proxies?: Array<{
name: string
type: string
[key: string]: any
}>
}
interface ProxyChainProps {
proxyChain: ProxyChainItem[]
onUpdateChain: (chain: ProxyChainItem[]) => void
chainConfigData?: string | null
onMarkUnsavedChanges?: () => void
mode?: string
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 {
proxy: ProxyChainItem
index: number
@@ -328,7 +303,6 @@ const SortableProxyChainItem = ({
export const ProxyChain = ({
proxyChain,
onUpdateChain,
chainConfigData,
onMarkUnsavedChanges,
mode,
selectedGroup,
@@ -580,23 +554,6 @@ export const ProxyChain = ({
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 (
<Paper
elevation={1}
+12 -3
View File
@@ -76,7 +76,7 @@ interface GroupSelectMenuProps {
interface ProxyGroupsChainProps {
mode: string
chainConfigData?: string | null
chainConfigItems?: ProxyChainItem[] | null
onExitChainMode: () => void
availableGroups: any[]
activeSelectedGroup: string | null
@@ -305,7 +305,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
const { t } = useTranslation()
const {
mode,
chainConfigData,
chainConfigItems,
onExitChainMode,
availableGroups,
activeSelectedGroup,
@@ -338,6 +338,16 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
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(
() =>
renderList.flatMap((item) => {
@@ -520,7 +530,6 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
<ProxyChain
proxyChain={currentProxyChain}
onUpdateChain={setProxyChain}
chainConfigData={chainConfigData}
mode={mode}
selectedGroup={activeSelectedGroup}
onExitChainMode={onExitChainMode}
+34 -8
View File
@@ -32,6 +32,7 @@ import {
} from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import type { ProxyChainItem } from './proxy-chain-model'
import { ProxyEmptyState } from './proxy-empty-state'
import {
resolveEmptyListReason,
@@ -68,7 +69,7 @@ function useStableCallback<T extends (...args: any[]) => any>(fn: T): T {
interface Props {
mode: string
isChainMode?: boolean
chainConfigData?: string | null
chainConfigItems?: ProxyChainItem[] | null
onExitChainMode: () => void
}
@@ -183,6 +184,7 @@ function useProxyRenderState(
renderList,
onProxies,
onHeadState,
setHeadState: setListHeadState,
handleCheckAll,
saveScrollPosition,
getScrollPosition,
@@ -191,10 +193,10 @@ function useProxyRenderState(
function ChainProxyGroups(props: {
mode: string
chainConfigData?: string | null
chainConfigItems?: ProxyChainItem[] | null
onExitChainMode: () => void
}) {
const { mode, chainConfigData, onExitChainMode } = props
const { mode, chainConfigItems, onExitChainMode } = props
const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
@@ -348,7 +350,7 @@ function ChainProxyGroups(props: {
<Suspense fallback={<BaseLoading />}>
<ProxyGroupsChain
mode={mode}
chainConfigData={chainConfigData}
chainConfigItems={chainConfigItems}
onExitChainMode={onExitChainMode}
availableGroups={availableGroups}
activeSelectedGroup={activeSelectedGroup}
@@ -377,12 +379,17 @@ function NormalProxyGroups(props: { mode: string }) {
renderList,
onProxies,
onHeadState,
setHeadState,
handleCheckAll,
getScrollPosition,
saveScrollPosition,
} = useProxyRenderState(mode, false, null)
const emptyList = useEmptyRenderList()
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)
// Do not persist intermediate positions produced while restoring virtual scroll.
const isRestoringRef = useRef(false)
@@ -477,9 +484,17 @@ function NormalProxyGroups(props: { mode: string }) {
if (!['Selector', 'URLTest', 'Fallback'].includes(group.type)) return
if (!isInteractableMember(member)) return
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!)
},
[select],
[globalGroupName, select, setHeadState],
)
const handleLocation = useStableCallback((group: ProxyGroupView) => {
@@ -525,6 +540,17 @@ function NormalProxyGroups(props: { mode: string }) {
return Array.from(new Set(names))
}, [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(
async (group: ProxyGroupView) => {
const index = renderList.findIndex(
@@ -608,7 +634,7 @@ function NormalProxyGroups(props: { mode: string }) {
/>
</DragDropProvider>
{(mode === 'rule' || mode === 'global') && (
{mode === 'rule' && (
<ProxyGroupNavigator
proxyGroupNames={proxyGroupNames}
onGroupLocation={handleGroupLocationByName}
@@ -621,7 +647,7 @@ function NormalProxyGroups(props: { mode: string }) {
}
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 { isProxyViewPending } = useProxiesData()
const { isRunningModePending } = useSystemData()
@@ -645,7 +671,7 @@ export const ProxyGroups = (props: Props) => {
return isChainMode ? (
<ChainProxyGroups
mode={mode}
chainConfigData={chainConfigData}
chainConfigItems={chainConfigItems}
onExitChainMode={onExitChainMode}
/>
) : (
+19 -18
View File
@@ -6,6 +6,7 @@ import { useVerge } from '@/hooks/use-verge'
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
import delayManager, { type DelaySnapshot } from '@/services/delay'
import {
findCurrentGroupMember,
isInteractableMember,
resolveMember,
selectGlobalChainNodes,
@@ -209,8 +210,10 @@ export const useRenderList = (
const renderGroups = useMemo(() => {
if (!proxyView) return []
if (mode === 'rule') return proxyView.groups
const global = proxyView.global ? [proxyView.global] : []
return mode === 'global' ? [...global, ...proxyView.groups] : global
const { global } = proxyView
if (!global) return []
const current = findCurrentGroupMember(proxyView, global)?.member
return current?.kind === 'group' ? [global, current.group] : [global]
}, [mode, proxyView])
// 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 retList = renderGroups.flatMap((group) => {
const isFlat = group === proxyView.global
const headState = headStates[group.name] || DEFAULT_STATE
const cached = cache.get(group.name)
if (
@@ -279,17 +282,19 @@ export const useRenderList = (
}
const ret: IRenderItem[] = [
{
type: 0,
key: group.name,
group,
headState,
icon: group.icon!,
testUrl: group.testUrl!,
},
isFlat
? { type: 1, key: `head-${group.name}`, group, headState }
: {
type: 0,
key: group.name,
group,
headState,
icon: group.icon!,
testUrl: group.testUrl!,
},
]
if (headState.open || !useRule) {
if (headState.open || isFlat) {
const occurrences = filterSort(
resolveOccurrences(proxyView, group),
group.name,
@@ -302,9 +307,6 @@ export const useRenderList = (
useRegularExpression: headState.filterUseRegularExpression,
},
)
if (!useRule) {
ret.push({ type: 1, key: `head-${group.name}`, group, headState })
}
if (occurrences.length === 0) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState })
} else if (col > 1) {
@@ -343,9 +345,8 @@ export const useRenderList = (
return ret
})
const filtered = !useRule
? retList.slice(1)
: retList.filter((item) => !item.group.hidden)
const filtered =
mode === 'rule' ? retList.filter((item) => !item.group.hidden) : retList
if (
prevListRef.current.length === filtered.length &&
filtered.every((item, index) => item === prevListRef.current[index])
@@ -146,7 +146,12 @@ export const BackupHistoryViewer = ({
setRows([])
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([])
return
}
@@ -180,7 +185,7 @@ export const BackupHistoryViewer = ({
setLoading(false)
}
},
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature],
)
useEffect(() => {
+122 -168
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> }) {
const { t } = useTranslation()
const { mutateClash } = useClash()
@@ -204,63 +308,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<MonacoEditorInstance | null>(null)
const [values, setValues] = useState<{
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: '',
})
const [values, setValues] = useState<DnsFormValues>(DEFAULT_FORM_VALUES)
// Prevent YAML-to-form sync before configuration loads.
const [yamlContent, setYamlContent] = useReducer(
@@ -269,7 +318,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
)
const updateValuesFromConfig = useCallback((config: any) => {
if (!config) return
if (!config) return undefined
const dnsConfig = config.dns || {}
const hostsConfig = config.hosts || {}
@@ -289,7 +338,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
? fakeIpFilterMode
: DEFAULT_DNS_CONFIG['fake-ip-filter-mode']
setValues({
const next: DnsFormValues = {
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode,
@@ -341,127 +390,27 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
nameserverPolicy:
formatNameserverPolicy(dnsConfig['nameserver-policy']) || '',
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(() => {
setValues({
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: '',
})
updateYamlFromValues()
}, [updateYamlFromValues])
setValues(DEFAULT_FORM_VALUES)
setYamlContent(buildDnsYaml(DEFAULT_FORM_VALUES))
}, [])
const updateValuesFromYaml = useCallback(() => {
try {
const parsedYaml = yaml.load(yamlContent) as any
if (!parsedYaml || parsedYaml.isInitValue) return
skipYamlSyncRef.current = true
updateValuesFromConfig(parsedYaml)
} catch {
showNotice.error('settings.modals.dns.errors.invalidYaml')
}
}, [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(() => {
return () => {
editorRef.current?.dispose()
@@ -476,8 +425,8 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (dnsConfig !== null) {
const config = yaml.load(dnsConfig) as any
updateValuesFromConfig(config)
setYamlContent(dnsConfig)
const loaded = updateValuesFromConfig(config)
setYamlContent(loaded ? buildDnsYaml(loaded) : dnsConfig)
} else {
resetToDefaults()
}
@@ -506,7 +455,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
if (visualization) {
config = {}
const dnsConfig = generateDnsConfig()
const dnsConfig = buildDnsConfig(values)
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig
}
@@ -619,7 +568,12 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev)
if (visualization) {
setYamlContent(buildDnsYaml(values))
} else {
updateValuesFromYaml()
}
setVisualization(!visualization)
}}
>
{visualization
@@ -13,13 +13,7 @@ import {
Typography,
} from '@mui/material'
import { useLockFn } from 'ahooks'
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react'
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
@@ -41,6 +35,7 @@ import {
getSystemHostname,
} from '@/services/cmds'
import { showNotice } from '@/services/notice-service'
import { useQuery } from '@/services/query-client'
import { debugLog } from '@/utils/debug'
import getSystem from '@/utils/get-system'
@@ -128,13 +123,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} = verge ?? {}
const [value, setValue] = useState(() => buildInitialValues(null))
const [embeddedServerPort, setEmbeddedServerPort] = useState<number | null>(
null,
)
useEffect(() => {
getEmbeddedServerPort().then(setEmbeddedServerPort).catch(console.error)
}, [])
const { data: embeddedServerPort } = useQuery({
queryKey: ['getEmbeddedServerPort'],
queryFn: getEmbeddedServerPort,
})
const separator = useMemo(() => (isWindows ? ';' : ','), [isWindows])
+9 -20
View File
@@ -1,4 +1,3 @@
/* eslint-disable @eslint-react/set-state-in-effect */
import { Delete, ExpandLess, ExpandMore } from '@mui/icons-material'
import {
Button,
@@ -14,7 +13,6 @@ import {
} from '@mui/material'
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useRef,
@@ -157,29 +155,20 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
const selectedProxyToken = selectedProxyOption
? proxyOptionToken(selectedProxyOption)
: ''
const selectedGroupExists =
!values.group || proxyGroups.some(({ name }) => name === values.group)
// A group or member missing from the proxy view reads as the default.
const effectiveGroup = proxyGroups.some(({ name }) => name === values.group)
? values.group
: ''
const effectiveProxy = selectedProxyOption ? values.proxy : null
const latestProxyStateRef = useRef({
binding: values.proxy,
binding: effectiveProxy,
options: proxyOptions,
})
latestProxyStateRef.current = {
binding: values.proxy,
binding: effectiveProxy,
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 () => {
try {
await patchClash({ tunnels: draftTunnels })
@@ -490,7 +479,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
<Select
size="small"
sx={{ width: 200, '> div': { py: '7.5px' } }}
value={values.group}
value={effectiveGroup}
displayEmpty
onChange={(e) => {
const nextGroup = e.target.value as string
@@ -569,7 +558,7 @@ export const TunnelsViewer = forwardRef<TunnelsViewerRef>((_, ref) => {
: null,
}))
}}
disabled={!values.group} // 没选组就禁用
disabled={!effectiveGroup} // 没选组就禁用
>
<MenuItem value="">
{t('settings.sections.clash.form.fields.tunnels.default')}
+18 -4
View File
@@ -7,6 +7,10 @@ import { useTranslation } from 'react-i18next'
import { BaseDialog } from '@/components/base'
import { useVerge } from '@/hooks/use-verge'
import type {
IVergeTestItem_Deserialize,
IVergeTestItem_Serialize,
} from '@/services/bindings'
import { showNotice } from '@/services/notice-service'
interface Props {
@@ -27,7 +31,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const [loading, setLoading] = useState(false)
const { verge, patchVerge } = useVerge()
const testList = verge?.test_list ?? []
const { control, ...formIns } = useForm<IVergeTestItem>({
const { control, ...formIns } = useForm<IVergeTestItem_Deserialize>({
defaultValues: {
name: '',
icon: '',
@@ -37,11 +41,16 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
const patchTestList = async (
uid: string,
patch: Partial<IVergeTestItem>,
patch: Partial<IVergeTestItem_Deserialize>,
) => {
const newList = testList.map((x) => {
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
})
@@ -77,7 +86,12 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
if (openType === 'new') {
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]
await patchVerge({ test_list: newList })
onChange(uid)
@@ -1,7 +1,7 @@
import { arrayMove } from '@dnd-kit/helpers'
import type { DragEndEvent } from '@dnd-kit/react'
import { isSortable } from '@dnd-kit/react/sortable'
import { useCallback, useEffect, useMemo, useReducer } from 'react'
import { useCallback, useMemo, useReducer, useState } from 'react'
type MenuOrderAction = { type: 'sync'; payload: string[] }
@@ -79,9 +79,11 @@ export const useNavMenuOrder = <T extends { path: string }>({
configMenuOrder,
)
useEffect(() => {
const [syncedConfigOrder, setSyncedConfigOrder] = useState(configMenuOrder)
if (!areOrdersEqual(syncedConfigOrder, configMenuOrder)) {
setSyncedConfigOrder(configMenuOrder)
dispatchMenuOrder({ type: 'sync', payload: configMenuOrder })
}, [configMenuOrder])
}
const isDefaultOrder = useMemo(
() => areOrdersEqual(menuOrder, defaultOrder),
+17 -18
View File
@@ -1,11 +1,15 @@
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
import { Box, Button, ButtonGroup, Typography } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
import { ProviderButton } from '@/components/proxy/provider-button'
import {
parseChainConfigItems,
type ProxyChainItem,
} from '@/components/proxy/proxy-chain-model'
import { ProxyGroups } from '@/components/proxy/proxy-groups'
import {
useAppRefreshers,
@@ -40,18 +44,13 @@ const ProxyPage = () => {
})
const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
const [chainConfigData, dispatchChainConfigData] = useReducer(
(_: string | null, action: string | null) => action,
null as string | null,
)
const [chainConfigItems, setChainConfigItems] = useState<
ProxyChainItem[] | null
>(null)
const { clashConfig } = useClashConfigData()
const { refreshClashConfig } = useAppRefreshers()
const updateChainConfigData = useCallback((value: string | null) => {
dispatchChainConfigData(value)
}, [])
const normalizedMode = clashConfig?.mode?.toLowerCase()
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
const chainWarning = t('proxies.page.chain.warning')
@@ -71,6 +70,7 @@ const ProxyPage = () => {
const applyChainMode = (enabled: boolean) => {
setIsChainMode(enabled)
if (!enabled) setChainConfigItems(null)
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
}
@@ -99,10 +99,7 @@ const ProxyPage = () => {
// 当开启链式代理模式时,获取配置数据
useEffect(() => {
if (!isChainMode) {
updateChainConfigData(null)
return
}
if (!isChainMode) return
let cancelled = false
@@ -113,19 +110,21 @@ const ProxyPage = () => {
if (!exitNode) {
console.error('No proxy chain exit node found in localStorage')
if (!cancelled) {
updateChainConfigData('')
setChainConfigItems([])
}
return
}
const configData = await getRuntimeProxyChainConfig(exitNode)
if (!cancelled) {
updateChainConfigData(configData || '')
setChainConfigItems(
configData ? parseChainConfigItems(configData) : [],
)
}
} catch (error) {
console.error('Failed to get runtime proxy chain config:', error)
if (!cancelled) {
updateChainConfigData('')
setChainConfigItems([])
}
}
}
@@ -135,7 +134,7 @@ const ProxyPage = () => {
return () => {
cancelled = true
}
}, [isChainMode, updateChainConfigData])
}, [isChainMode])
useEffect(() => {
if (normalizedMode && !isMode(normalizedMode)) {
@@ -204,7 +203,7 @@ const ProxyPage = () => {
<ProxyGroups
mode={curMode ?? 'rule'}
isChainMode={isChainMode}
chainConfigData={chainConfigData}
chainConfigItems={chainConfigItems}
onExitChainMode={onExitChainMode}
/>
<BaseDialog
+261 -155
View File
@@ -23,7 +23,10 @@ export const commands = {
__TAURI_INVOKE<string[]>('get_network_interfaces'),
getSystemHostname: () => __TAURI_INVOKE<string>('get_system_hostname'),
restartApp: () => __TAURI_INVOKE<null>('restart_app'),
installUpdate: (version: string, onEvent: Channel<DownloadEvent>) =>
installUpdate: (
version: string,
onEvent: Channel<DownloadEvent_Deserialize>,
) =>
__TAURI_INVOKE<InstallUpdateOutcome>('install_update', {
version,
onEvent,
@@ -34,11 +37,12 @@ export const commands = {
upgradeClashCore: (force: boolean) =>
__TAURI_INVOKE<CoreUpgradeReport>('upgrade_clash_core', { force }),
/** Returns one coherent core/service snapshot instead of independently refreshed state. */
getRuntimeState: () => __TAURI_INVOKE<RunStateView>('get_runtime_state'),
getRuntimeState: () =>
__TAURI_INVOKE<RunStateView_Serialize>('get_runtime_state'),
getPendingFailures: () =>
__TAURI_INVOKE<PendingFailure[]>('get_pending_failures'),
getSidecarFailure: () =>
__TAURI_INVOKE<SidecarFailureSnapshot>('get_sidecar_failure'),
__TAURI_INVOKE<SidecarFailureSnapshot_Serialize>('get_sidecar_failure'),
entryLightweightMode: () => __TAURI_INVOKE<null>('entry_lightweight_mode'),
installService: () =>
__TAURI_INVOKE<ServiceInstallOutcome>('install_service'),
@@ -48,7 +52,7 @@ export const commands = {
repairService: () => __TAURI_INVOKE<ServiceInstallOutcome>('repair_service'),
continueWithSidecar: () => __TAURI_INVOKE<null>('continue_with_sidecar'),
syncRuntimeProviders: () => __TAURI_INVOKE<void>('sync_runtime_providers'),
getClashInfo: () => __TAURI_INVOKE<ClashInfo>('get_clash_info'),
getClashInfo: () => __TAURI_INVOKE<ClashInfo_Serialize>('get_clash_info'),
patchClashConfig: (payload: YamlMapping) =>
__TAURI_INVOKE<null>('patch_clash_config', { payload }),
patchClashMode: (payload: string) =>
@@ -56,16 +60,11 @@ export const commands = {
/** Reads the saved mode without depending on strict mihomo `/configs` deserialization. */
getClashMode: () => __TAURI_INVOKE<string | null>('get_clash_mode'),
changeClashCore: (clashCore: string) =>
__TAURI_INVOKE<{
/** Stable frontend identifier, when classified. */
code?: string | null
/** Human-readable diagnostic detail. */
detail: string
/** Requested operation when known by the command. */
operation?: FailedOperation | null
} | null>('change_clash_core', { clashCore }),
__TAURI_INVOKE<CommandFailure_Serialize | null>('change_clash_core', {
clashCore,
}),
getRuntimeConfig: () =>
__TAURI_INVOKE<{ [key in string]: JsonValue } | null>('get_runtime_config'),
__TAURI_INVOKE<YamlMapping | null>('get_runtime_config'),
getProxyView: () => __TAURI_INVOKE<ProxyViewV1_Serialize>('get_proxy_view'),
getRuntimeYaml: () => __TAURI_INVOKE<string>('get_runtime_yaml'),
getRuntimeLogs: () => __TAURI_INVOKE<RuntimeLogs>('get_runtime_logs'),
@@ -73,7 +72,7 @@ export const commands = {
__TAURI_INVOKE<string>('get_runtime_proxy_chain_config', {
proxyChainExitNode,
}),
updateProxyChainConfigInRuntime: (proxyChainConfig: JsonValue | null) =>
updateProxyChainConfigInRuntime: (proxyChainConfig: YamlValue | null) =>
__TAURI_INVOKE<ValidationOutcome>('update_proxy_chain_config_in_runtime', {
proxyChainConfig,
}),
@@ -103,18 +102,11 @@ export const commands = {
takeDnsOverrideNotice: () =>
__TAURI_INVOKE<boolean>('take_dns_override_notice'),
takeServiceFallbackNotice: () =>
__TAURI_INVOKE<
| { kind: 'unavailable' }
| { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
| null
>('take_service_fallback_notice'),
__TAURI_INVOKE<ServiceFallbackNotice | null>(
'take_service_fallback_notice',
),
getCoreStartupError: () =>
__TAURI_INVOKE<
| { kind: 'startFailed'; detail: string }
| { kind: 'serviceCoreStopped'; detail: string }
| null
>('get_core_startup_error'),
__TAURI_INVOKE<CoreFailure | null>('get_core_startup_error'),
takeServiceRepairNotice: () =>
__TAURI_INVOKE<boolean>('take_service_repair_notice'),
takeServiceOwnerNotice: () =>
@@ -140,10 +132,12 @@ export const commands = {
openDevtools: () => __TAURI_INVOKE<void>('open_devtools'),
exitApp: () => __TAURI_INVOKE<void>('exit_app'),
getNetworkInterfacesInfo: () =>
__TAURI_INVOKE<NetworkInterfaceView[]>('get_network_interfaces_info'),
__TAURI_INVOKE<NetworkInterfaceView_Serialize[]>(
'get_network_interfaces_info',
),
getProfiles: () => __TAURI_INVOKE<IProfiles_Serialize>('get_profiles'),
enhanceProfiles: () => __TAURI_INVOKE<ValidationOutcome>('enhance_profiles'),
patchProfilesConfig: (profiles: ProfileSelection) =>
patchProfilesConfig: (profiles: ProfileSelection_Deserialize) =>
__TAURI_INVOKE<ValidationOutcome>('patch_profiles_config', { profiles }),
viewProfile: (index: string) =>
__TAURI_INVOKE<null>('view_profile', { index }),
@@ -151,52 +145,12 @@ export const commands = {
__TAURI_INVOKE<null>('patch_profile', { index, profile }),
createProfile: (item: ProfileCreate_Deserialize, fileData: string | null) =>
__TAURI_INVOKE<null>('create_profile', { item, fileData }),
importProfile: (
url: string,
option: {
user_agent?: string | null
with_proxy?: boolean | null
self_proxy?: boolean | null
update_interval?: number | null
/**
* HTTP request timeout in seconds
* default is 60 seconds
*/
timeout_seconds?: number | null
/** default is `false` */
danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean | null
merge?: string | null
script?: string | null
rules?: string | null
proxies?: string | null
groups?: string | null
} | null,
) => __TAURI_INVOKE<null>('import_profile', { url, option }),
importProfile: (url: string, option: PrfOption_Deserialize | null) =>
__TAURI_INVOKE<null>('import_profile', { url, option }),
reorderProfile: (activeId: string, overId: string) =>
__TAURI_INVOKE<null>('reorder_profile', { activeId, overId }),
updateProfile: (
index: string,
option: {
user_agent?: string | null
with_proxy?: boolean | null
self_proxy?: boolean | null
update_interval?: number | null
/**
* HTTP request timeout in seconds
* default is 60 seconds
*/
timeout_seconds?: number | null
/** default is `false` */
danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean | null
merge?: string | null
script?: string | null
rules?: string | null
proxies?: string | null
groups?: string | null
} | null,
) => __TAURI_INVOKE<null>('update_profile', { index, option }),
updateProfile: (index: string, option: PrfOption_Deserialize | null) =>
__TAURI_INVOKE<null>('update_profile', { index, option }),
deleteProfile: (index: string) =>
__TAURI_INVOKE<null>('delete_profile', { index }),
readProfileFile: (index: string) =>
@@ -224,11 +178,14 @@ export const commands = {
__TAURI_INVOKE<null>('delete_webdav_backup', { filename }),
restoreWebdavBackup: (filename: string) =>
__TAURI_INVOKE<null>('restore_webdav_backup', { filename }),
getUnlockItems: () => __TAURI_INVOKE<UnlockItem[]>('get_unlock_items'),
checkMediaUnlock: (onComplete: Channel<UnlockItem>) =>
__TAURI_INVOKE<UnlockItem[]>('check_media_unlock', { onComplete }),
getUnlockItems: () =>
__TAURI_INVOKE<UnlockItem_Serialize[]>('get_unlock_items'),
checkMediaUnlock: (onComplete: Channel<UnlockItem_Deserialize>) =>
__TAURI_INVOKE<UnlockItem_Serialize[]>('check_media_unlock', {
onComplete,
}),
checkMediaUnlockItem: (name: string) =>
__TAURI_INVOKE<UnlockItem>('check_media_unlock_item', { name }),
__TAURI_INVOKE<UnlockItem_Serialize>('check_media_unlock_item', { name }),
}
/* Types */
@@ -237,7 +194,20 @@ export type AutoProxy = {
url: string
}
export type ClashInfo = {
export type ClashInfo = ClashInfo_Serialize | ClashInfo_Deserialize
export type ClashInfo_Deserialize = {
/** clash core port */
mixed_port: number
socks_port: number
port: number
/** same as `external-controller` */
server: string
/** clash secret */
secret?: string | null
}
export type ClashInfo_Serialize = {
/** clash core port */
mixed_port: number
socks_port: number
@@ -255,16 +225,6 @@ export type CommandFailure =
/** Structured command failure returned to the frontend. */
export type CommandFailure_Deserialize = {
/** Stable frontend identifier, when classified. */
code: string | null
/** Human-readable diagnostic detail. */
detail: string
/** Requested operation when known by the command. */
operation: FailedOperation | null
}
/** Structured command failure returned to the frontend. */
export type CommandFailure_Serialize = {
/** Stable frontend identifier, when classified. */
code?: string | null
/** Human-readable diagnostic detail. */
@@ -273,6 +233,16 @@ export type CommandFailure_Serialize = {
operation?: FailedOperation | null
}
/** Structured command failure returned to the frontend. */
export type CommandFailure_Serialize = {
/** Stable frontend identifier, when classified. */
code?: string
/** Human-readable diagnostic detail. */
detail: string
/** Requested operation when known by the command. */
operation?: FailedOperation
}
/** Why the Core is not running, kept until a Core starts again. */
export type CoreFailure =
| { kind: 'startFailed'; detail: string }
@@ -293,7 +263,26 @@ export type DnsOverrideOutcome =
| { status: 'confirmation_required'; source: string }
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent =
export type DownloadEvent = DownloadEvent_Serialize | DownloadEvent_Deserialize
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent_Deserialize =
| {
event: 'Started'
data: {
contentLength?: number | null
}
}
| {
event: 'Progress'
data: {
chunkLength: number
}
}
| { event: 'Finished' }
/** Same wire shape as the updater plugin's JS `DownloadEvent`. */
export type DownloadEvent_Serialize =
| {
event: 'Started'
data: {
@@ -333,14 +322,27 @@ export type IProfiles_Serialize = {
/** ### `verge.yaml` schema */
export type IVerge = IVerge_Serialize | IVerge_Deserialize
export type IVergeTestItem = {
export type IVergeTestItem =
| IVergeTestItem_Serialize
| IVergeTestItem_Deserialize
export type IVergeTestItem_Deserialize = {
uid?: string | null
name?: string | null
icon?: string | null
url?: string | null
}
export type IVergeTheme = {
export type IVergeTestItem_Serialize = {
uid: string | null
name: string | null
icon: string | null
url: string | null
}
export type IVergeTheme = IVergeTheme_Serialize | IVergeTheme_Deserialize
export type IVergeTheme_Deserialize = {
primary_color?: string | null
secondary_color?: string | null
primary_text?: string | null
@@ -353,6 +355,19 @@ export type IVergeTheme = {
css_injection?: string | null
}
export type IVergeTheme_Serialize = {
primary_color: string | null
secondary_color: string | null
primary_text: string | null
secondary_text: string | null
info_color: string | null
error_color: string | null
warning_color: string | null
success_color: string | null
font_family: string | null
css_injection: string | null
}
/** ### `verge.yaml` schema */
export type IVerge_Deserialize = {
/** silent | error | warn | info | debug | trace */
@@ -395,7 +410,7 @@ export type IVerge_Deserialize = {
proxy_auto_config?: boolean | null
pac_file_content?: string | null
proxy_host?: string | null
theme_setting?: IVergeTheme | null
theme_setting?: IVergeTheme_Deserialize | null
web_ui_list?: string[] | null
clash_core?: string | null
/** format: {func},{key} */
@@ -411,7 +426,7 @@ export type IVerge_Deserialize = {
auto_delay_detection_interval_minutes?: number | null
enable_builtin_enhanced?: boolean | null
proxy_layout_column?: number | null
test_list?: IVergeTestItem[] | null
test_list?: IVergeTestItem_Deserialize[] | null
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
auto_log_clean?: number | null
enable_auto_backup_schedule?: boolean | null
@@ -460,13 +475,13 @@ export type IVerge_Serialize = {
traffic_graph: boolean | null
/** show memory info (only for Mihomo) */
enable_memory_usage: boolean | null
enable_group_icon?: boolean | null
enable_group_icon?: boolean
pause_render_traffic_stats_on_blur: boolean | null
common_tray_icon?: boolean | null
menu_icon?: string | null
menu_order?: string[] | null
proxy_group_tools_position?: string | null
notice_position?: string | null
common_tray_icon?: boolean
menu_icon?: string
menu_order?: string[]
proxy_group_tools_position?: string
notice_position?: string
collapse_navbar: boolean | null
sysproxy_tray_icon: boolean | null
tun_tray_icon: boolean | null
@@ -485,11 +500,11 @@ export type IVerge_Serialize = {
proxy_auto_config: boolean | null
pac_file_content: string | null
proxy_host: string | null
theme_setting: IVergeTheme | null
theme_setting: IVergeTheme_Serialize | null
web_ui_list: string[] | null
clash_core?: string | null
clash_core?: string
/** format: {func},{key} */
hotkeys?: string[] | null
hotkeys?: string[]
enable_global_hotkey: boolean | null
home_cards: JsonValue | null
auto_close_connection: boolean | null
@@ -501,7 +516,7 @@ export type IVerge_Serialize = {
auto_delay_detection_interval_minutes: number | null
enable_builtin_enhanced: boolean | null
proxy_layout_column: number | null
test_list: IVergeTestItem[] | null
test_list: IVergeTestItem_Serialize[] | null
/** 0: 不清理; 1: 1天;2: 7天; 3: 30天; 4: 90天 */
auto_log_clean: number | null
enable_auto_backup_schedule: boolean | null
@@ -513,10 +528,10 @@ export type IVerge_Serialize = {
verge_socks_enabled: boolean | null
verge_port: number | null
verge_http_enabled: boolean | null
webdav_url?: string | null
webdav_username?: string | null
webdav_password?: string | null
tray_proxy_groups_display_mode?: string | null
webdav_url?: string
webdav_username?: string
webdav_password?: string
tray_proxy_groups_display_mode?: string
tray_inline_outbound_modes: boolean | null
enable_auto_light_weight_mode: boolean | null
/** 自动进入轻量模式的延迟(分钟) */
@@ -578,7 +593,17 @@ export type LocalBackupFile = {
content_length: number
}
export type NetworkInterfaceView = {
export type NetworkInterfaceView =
| NetworkInterfaceView_Serialize
| NetworkInterfaceView_Deserialize
export type NetworkInterfaceView_Deserialize = {
name: string
addr: InterfaceAddress[]
mac_addr?: string | null
}
export type NetworkInterfaceView_Serialize = {
name: string
addr: InterfaceAddress[]
mac_addr: string | null
@@ -637,7 +662,20 @@ export type PendingFailure = {
sequence: number
}
export type PlatformVergeFields = {
export type PlatformVergeFields =
| PlatformVergeFields_Serialize
| PlatformVergeFields_Deserialize
export type PlatformVergeFields_Deserialize = {
tray_icon?: string | null
verge_redir_port?: number | null
verge_redir_enabled?: boolean | null
verge_tproxy_port?: number | null
verge_tproxy_enabled?: boolean | null
enable_tray_speed?: boolean | null
}
export type PlatformVergeFields_Serialize = {
tray_icon?: string | null
verge_redir_port?: number | null
verge_redir_enabled?: boolean | null
@@ -663,7 +701,7 @@ export type PrfItem_Deserialize = {
file?: string | null
desc?: string | null
url?: string | null
selected?: PrfSelected[] | null
selected?: PrfSelected_Deserialize[] | null
extra?: PrfExtra | null
updated?: number | null
option?: PrfOption_Deserialize | null
@@ -676,13 +714,13 @@ export type PrfItem_Serialize = {
type: string | null
name: string | null
file: string | null
desc?: string | null
url?: string | null
selected?: PrfSelected[] | null
extra?: PrfExtra | null
desc?: string
url?: string
selected?: PrfSelected_Serialize[]
extra?: PrfExtra
updated: number | null
option?: PrfOption_Serialize | null
home?: string | null
option?: PrfOption_Serialize
home?: string
}
export type PrfOption = PrfOption_Serialize | PrfOption_Deserialize
@@ -708,18 +746,18 @@ export type PrfOption_Deserialize = {
}
export type PrfOption_Serialize = {
user_agent?: string | null
with_proxy?: boolean | null
self_proxy?: boolean | null
update_interval?: number | null
user_agent?: string
with_proxy?: boolean
self_proxy?: boolean
update_interval?: number
/**
* HTTP request timeout in seconds
* default is 60 seconds
*/
timeout_seconds?: number | null
timeout_seconds?: number
/** default is `false` */
danger_accept_invalid_certs?: boolean | null
allow_auto_update?: boolean | null
danger_accept_invalid_certs?: boolean
allow_auto_update?: boolean
merge: string | null
script: string | null
rules: string | null
@@ -727,11 +765,18 @@ export type PrfOption_Serialize = {
groups: string | null
}
export type PrfSelected = {
export type PrfSelected = PrfSelected_Serialize | PrfSelected_Deserialize
export type PrfSelected_Deserialize = {
name?: string | null
now?: string | null
}
export type PrfSelected_Serialize = {
name: string | null
now: string | null
}
export type ProfileCreate = ProfileCreate_Serialize | ProfileCreate_Deserialize
export type ProfileCreate_Deserialize = {
@@ -761,7 +806,7 @@ export type ProfileDnsSettings_Deserialize = {
export type ProfileDnsSettings_Serialize = {
enabled: boolean
confirmation?: string | null
confirmation?: string
}
export type ProfilePatch = ProfilePatch_Serialize | ProfilePatch_Deserialize
@@ -772,7 +817,7 @@ export type ProfilePatch_Deserialize = {
desc?: string | null
file?: string | null
url?: string | null
selected?: PrfSelected[] | null
selected?: PrfSelected_Deserialize[] | null
extra?: PrfExtra | null
updated?: number | null
option?: PrfOption_Deserialize | null
@@ -784,16 +829,24 @@ export type ProfilePatch_Serialize = {
desc: string | null
file: string | null
url: string | null
selected: PrfSelected[] | null
selected: PrfSelected_Serialize[] | null
extra: PrfExtra | null
updated: number | null
option: PrfOption_Serialize | null
}
export type ProfileSelection = {
export type ProfileSelection =
| ProfileSelection_Serialize
| ProfileSelection_Deserialize
export type ProfileSelection_Deserialize = {
current?: string | null
}
export type ProfileSelection_Serialize = {
current: string | null
}
export type ProfileUpdatePayload = {
uid: string
}
@@ -813,11 +866,11 @@ export type ProxyGroupView =
export type ProxyGroupView_Deserialize = {
name: string
type: string
now: string | null
fixed: string | null
hidden: boolean | null
icon: string | null
testUrl: string | null
now?: string | null
fixed?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
history: DelayHistory[]
members: ProxyMemberRef[]
} & ProxyCapabilities
@@ -825,11 +878,11 @@ export type ProxyGroupView_Deserialize = {
export type ProxyGroupView_Serialize = {
name: string
type: string
now?: string | null
fixed?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
now?: string
fixed?: string
hidden?: boolean
icon?: string
testUrl?: string
history: DelayHistory[]
members: ProxyMemberRef[]
} & ProxyCapabilities
@@ -855,10 +908,10 @@ export type ProxyNodeView_Deserialize = {
name: string
type: string
history: DelayHistory[]
id: string | null
hidden: boolean | null
icon: string | null
testUrl: string | null
id?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
source: ProxyNodeSource
} & ProxyCapabilities
@@ -867,10 +920,10 @@ export type ProxyNodeView_Serialize = {
name: string
type: string
history: DelayHistory[]
id?: string | null
hidden?: boolean | null
icon?: string | null
testUrl?: string | null
id?: string
hidden?: boolean
icon?: string
testUrl?: string
source: ProxyNodeSource
} & ProxyCapabilities
@@ -891,16 +944,16 @@ export type ProxyProviderView =
export type ProxyProviderView_Deserialize = {
name: string
vehicleType: ProxyProviderVehicleType
updatedAt: string | null
subscriptionInfo: ProxySubscriptionInfo | null
updatedAt?: string | null
subscriptionInfo?: ProxySubscriptionInfo | null
proxyRecordIds: string[]
}
export type ProxyProviderView_Serialize = {
name: string
vehicleType: ProxyProviderVehicleType
updatedAt?: string | null
subscriptionInfo?: ProxySubscriptionInfo | null
updatedAt?: string
subscriptionInfo?: ProxySubscriptionInfo
proxyRecordIds: string[]
}
@@ -921,8 +974,8 @@ export type ProxyViewV1_Deserialize = {
schemaVersion: number
orderSource: ProxyViewOrderSource
providerState: ProxyViewProviderState
global: ProxyGroupView_Deserialize | null
direct: string | null
global?: ProxyGroupView_Deserialize | null
direct?: string | null
groups: ProxyGroupView_Deserialize[]
records: { [key in string]: ProxyNodeView_Deserialize }
standalone: string[]
@@ -941,7 +994,22 @@ export type ProxyViewV1_Serialize = {
providers: ProxyProviderView_Serialize[]
}
export type RunStateView = {
export type RunStateView = RunStateView_Serialize | RunStateView_Deserialize
export type RunStateView_Deserialize = {
mode: RunningMode
service: ServiceHealthKind
serviceUnavailableReason?: string | null
pendingAction?: PendingAction | null
sidecarAllowed: boolean
isAdmin: boolean
opInFlight: boolean
serviceUsable: boolean
tunCapable: boolean
serviceNeedsAttention: boolean
}
export type RunStateView_Serialize = {
mode: RunningMode
service: ServiceHealthKind
serviceUnavailableReason: string | null
@@ -980,7 +1048,18 @@ export type ServiceInstallOutcome =
| { status: 'sidecar'; reason: string }
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot = {
export type SidecarFailureSnapshot =
| SidecarFailureSnapshot_Serialize
| SidecarFailureSnapshot_Deserialize
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot_Deserialize = {
revision: number
detail?: string | null
}
/** A removal has a revision too, so a delayed read cannot restore a resolved failure. */
export type SidecarFailureSnapshot_Serialize = {
revision: number
detail: string | null
}
@@ -1001,7 +1080,16 @@ export type ToggleableProxyPort = {
port: number
}
export type UnlockItem = {
export type UnlockItem = UnlockItem_Serialize | UnlockItem_Deserialize
export type UnlockItem_Deserialize = {
name: string
status: string
region?: string | null
check_time?: string | null
}
export type UnlockItem_Serialize = {
name: string
status: string
region: string | null
@@ -1027,7 +1115,11 @@ export type ValidationOutcome =
export type ValidationSkipReason = 'exiting' | 'debounced'
export type VergeEventPayloads = {
export type VergeEventPayloads =
| VergeEventPayloads_Serialize
| VergeEventPayloads_Deserialize
export type VergeEventPayloads_Deserialize = {
'verge://refresh-clash-config': null
'verge://refresh-verge-config': null
'verge://refresh-profiles': null
@@ -1037,7 +1129,21 @@ export type VergeEventPayloads = {
'verge://timer-updated': string
'profile-update-started': ProfileUpdatePayload
'profile-update-completed': ProfileUpdatePayload
'verge://run-state-changed': RunStateView
'verge://run-state-changed': RunStateView_Deserialize
'verge://pending-failures-changed': null
}
export type VergeEventPayloads_Serialize = {
'verge://refresh-clash-config': null
'verge://refresh-verge-config': null
'verge://refresh-profiles': null
'verge://refresh-proxy-config': null
'verge://notice-message': [NoticeStatus, string]
'profile-changed': string
'verge://timer-updated': string
'profile-update-started': ProfileUpdatePayload
'profile-update-completed': ProfileUpdatePayload
'verge://run-state-changed': RunStateView_Serialize
'verge://pending-failures-changed': null
}
+1 -1
View File
@@ -19,7 +19,7 @@ import type {
export type {
RunningMode,
RunStateView as RunState,
RunStateView_Serialize as RunState,
FailedOperation,
PendingFailure,
SidecarFailureSnapshot,
+4 -1
View File
@@ -1 +1,4 @@
export type { NoticeStatus, VergeEventPayloads } from './bindings'
export type {
NoticeStatus,
VergeEventPayloads_Serialize as VergeEventPayloads,
} from './bindings'
+1
View File
@@ -939,6 +939,7 @@ export const translationKeys = [
'tests.statuses.test.yes',
'tests.statuses.test.no',
'tests.statuses.test.failed',
'tests.statuses.test.risky',
'tests.statuses.test.completed',
'tests.statuses.test.disallowedIsp',
'tests.statuses.test.originalsOnly',
+1
View File
@@ -1589,6 +1589,7 @@ export interface TranslationResources {
noDisney: string
originalsOnly: string
pending: string
risky: string
unsupportedRegion: string
yes: string
}