Compare commits

...
16 Commits
Author SHA1 Message Date
wonfen 4e5ac8683b feat(proxy): confirm before enabling chain proxy
Show the speed warning with explicit enable/keep-disabled actions, and add a close button to the empty chain panel.
2026-10-19 20:01:43 +08:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>❤是纱雾酱哟~
7965711945 chore(deps): update cargo dependencies (#8323)
* chore(deps): update cargo dependencies

* chore(deps): revert dependency rfd to 0.16.0

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-10-17 16:14:23 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 396d343a3a chore(deps): update npm dependencies (#8335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 13:40:23 +00:00
❤是纱雾酱哟~ b729e59650 Revert "ci(workflows): bump gh-aw from 0.89.21 to 0.92.0 (#8330)" (#8343)
This reverts commit 089635c8bf.
2026-10-17 11:04:13 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> a3cc58013b chore(deps): update rust toolchain to v1.99.1 (#8341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 09:55:28 +00:00
❤是纱雾酱哟~andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> cef66cd745 chore(deps): update dependency pnpm to v12.14.0 (#8340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-17 09:04:27 +00:00
Tunglies 3b7bfa10fd chore(deps): label the tauri group PR
Tag the coordinated Tauri update PR with a dedicated tauri label so it
stands out from the generic dependency groups.
2026-10-17 13:01:03 +08:00
Tunglies b238175f18 chore(deps): add @wdio/tauri-service to the tauri update group
The WebdriverIO Tauri service pairs with the Tauri stack it drives, so
its updates belong with the coordinated Tauri npm and Cargo upgrades
rather than the generic npm group.
2026-10-17 12:53:14 +08:00
Tunglies 8f00096a9f chore(deps): group Tauri npm and Cargo updates into a single PR
Add a packageRule that pulls @tauri-apps/* packages and tauri/tauri-*
crates out of the generic npm and cargo groups into one shared "tauri"
group, so frontend and backend halves of a Tauri upgrade are validated
together.

Pin tauri-plugin-mihomo by rev instead of branch so Renovate tracks it:
branch-pinned git dependencies are skipped with skipReason
git-dependency and never produce updates. With the rev pin the plugin
joins the tauri group via digest updates. The rev is the commit already
locked in Cargo.lock, so resolution is unchanged.
2026-10-17 12:40:05 +08:00
Tunglies 9f498a85d0 perf(frontend): hoist notice table and skip no-op loading updates
The notice handler table was rebuilt on every notice event; build it
once at module scope. withLoading returned a new state even when the
loading set was unchanged, re-rendering store consumers for nothing.
2026-10-17 10:32:15 +08:00
Tunglies e014b913d2 fix(profile): close switching-flag leak window and drop duplicate busy notice
The switching flag was released by a defer registered inside
perform_config_update, so a panic during the write-lock wait or the
draft edit could leave it stuck set. Register the defer right after the
CAS. Skip the generic validation notice for busy switch outcomes; the
frontend already shows its own busy toast.
2026-10-17 09:14:57 +08:00
Tunglies f38ceae3fb fix(profiles): restore single switch error toast and refetch after busy
patchProfiles entered the mutate funnel without errorNotice:false, so a
failed switch toasted twice. On busy the cached current profile was left
untouched, so a discarded switch draft could poison later clicks on the
target profile. Refetch on every non-valid outcome.
2026-10-17 08:10:16 +08:00
❤是纱雾酱哟~ 089635c8bf ci(workflows): bump gh-aw from 0.89.21 to 0.92.0 (#8330)
- Bump `gh-aw` from 0.89.21 to 0.92.0
- Switch from `pull_request_target` to `pull_request` for CI security
- Set `permissions.copilot-requests` to `none` for suppressing compilation info
2026-10-16 12:11:40 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> dc5a8a5ff4 chore(deps): update node.js to v24.22.0 (#8324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-10-16 06:25:50 +00:00
Tunglies 476ddb63c3 fix(proxy): revalidate proxy view when expanding a group
Badge values for freshly expanded rows seed from the cached proxy
view. Results that reach the core without a frontend event (tray
group test, provider health checks) are absent from that snapshot
until the next poll, so measured delays only appeared after another
interaction such as switching nodes forced a refetch. Route head
state patches through a wrapper that revalidates the view on
open transitions, matching how the group test already refreshes.
2026-10-16 06:44:54 +08:00
c6b51171a1 fix(backup): keep WebDAV password masked (#8291)
* fix(backup): keep WebDAV password masked

* fix(eslint): Clean unused imports

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>

---------

Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
Co-authored-by: susu-Sugar <214122492+susu-Sugar@users.noreply.github.com>
Co-authored-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com>
2026-10-14 16:52:11 +00:00
44 changed files with 746 additions and 686 deletions
+3 -3
View File
@@ -4,11 +4,11 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "24.21.0",
"pnpmVersion": "12.11.2"
"version": "24.22.0",
"pnpmVersion": "12.14.0"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.99.0",
"version": "1.99.1",
"profile": "default"
},
"ghcr.io/devcontainers/features/git:1": {},
+10 -10
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -154,7 +154,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -185,7 +185,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -206,7 +206,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -257,7 +257,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -282,7 +282,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -347,7 +347,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -426,7 +426,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -454,7 +454,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -524,7 +524,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6.1.0
name: Install pnpm
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+3 -3
View File
@@ -74,7 +74,7 @@ jobs:
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@1.99.0
uses: dtolnay/rust-toolchain@1.99.1
- name: Rust Cache
uses: Swatinem/rust-cache@v2
@@ -103,7 +103,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
cache: 'pnpm'
- name: Pnpm Cache
@@ -129,7 +129,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
+2 -2
View File
@@ -52,7 +52,7 @@ jobs:
- uses: actions/setup-node@v7
if: steps.check_frontend.outputs.frontend == 'true'
with:
node-version: '24.21.0'
node-version: '24.22.0'
cache: 'pnpm'
- name: Restore pnpm cache
@@ -138,7 +138,7 @@ jobs:
- uses: actions/setup-node@v7
if: steps.check_inputs.outputs.contract == 'true'
with:
node-version: '24.21.0'
node-version: '24.22.0'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
+12 -12
View File
@@ -204,7 +204,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -230,7 +230,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -244,7 +244,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -311,7 +311,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -331,7 +331,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- name: Install pnpm
uses: pnpm/action-setup@v6
@@ -389,7 +389,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -446,7 +446,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Add Rust Target
@@ -466,7 +466,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -487,7 +487,7 @@ jobs:
- name: Add Rust Target
run: |
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
@@ -547,7 +547,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -573,7 +573,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -638,7 +638,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -22,13 +22,13 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.99.0'
toolchain: '1.99.1'
targets: x86_64-pc-windows-msvc
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- name: Install pnpm
uses: pnpm/action-setup@v6
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '24.21.0'
node-version: '24.22.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+3 -3
View File
@@ -1,3 +1,3 @@
nodejs 24.21.0
pnpm 12.11.2
rust 1.99.0
nodejs 24.22.0
pnpm 12.14.0
rust 1.99.1
Generated
+34 -31
View File
@@ -1138,7 +1138,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml_ng",
"sha2 0.11.0",
"sha2 0.11.1",
"smartstring",
"socket2",
"specta",
@@ -1175,7 +1175,7 @@ dependencies = [
"windows-service",
"windows-sys 0.61.2",
"winreg 0.56.0",
"zip 9.0.1",
"zip 9.0.4",
]
[[package]]
@@ -1247,7 +1247,7 @@ dependencies = [
"once_cell",
"serde",
"serde_json",
"sha2 0.11.0",
"sha2 0.11.1",
"strum",
"strum_macros",
"sysproxy",
@@ -1462,9 +1462,12 @@ dependencies = [
[[package]]
name = "constant_time_eq"
version = "0.4.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
checksum = "9eb6feb175df65d0a3ccd3e73484e940d0ac664e8034f0519fd49a93db3071d9"
dependencies = [
"rustversion",
]
[[package]]
name = "cookie"
@@ -3951,9 +3954,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
[[package]]
name = "libc"
version = "0.2.190"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libdbus-sys"
@@ -4048,11 +4051,11 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
version = "0.21.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde178a3caf126c440fa15628147772d8ee590a39477d711353fbe2e58a73a5b"
checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb"
dependencies = [
"sha2 0.11.0",
"sha2 0.11.1",
]
[[package]]
@@ -5343,9 +5346,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppmd-rust"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
checksum = "196a7c80b9a7652aba7cc070827516c2abe4ccdf53d128e1944003cf5726cff1"
[[package]]
name = "ppv-lite86"
@@ -6067,9 +6070,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.45"
version = "0.23.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
dependencies = [
"aws-lc-rs",
"log",
@@ -6410,9 +6413,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.151"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
@@ -6597,9 +6600,9 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.11.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
@@ -7492,7 +7495,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-mihomo"
version = "0.6.0"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=main#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?rev=7a03d16f4d39b1bb885b7babe66b9e153e34b615#7a03d16f4d39b1bb885b7babe66b9e153e34b615"
dependencies = [
"arc-swap",
"clashmap",
@@ -8073,9 +8076,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.53.2"
version = "1.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
dependencies = [
"bytes",
"libc",
@@ -8135,9 +8138,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.19"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
dependencies = [
"bytes",
"futures-core",
@@ -10150,9 +10153,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.0.1"
version = "9.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e49ade69c4434c55dd651d36fe05f38e0578db948b773cbc01c48c4b5e560692"
checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22"
dependencies = [
"aes",
"bzip2",
@@ -10202,27 +10205,27 @@ dependencies = [
[[package]]
name = "zstd"
version = "0.13.3"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
version = "2.1.2+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
dependencies = [
"cc",
"pkg-config",
+2 -2
View File
@@ -67,7 +67,7 @@ parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
chrono = "0.4.45"
criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.53.2", features = [
tokio = { version = "1.53.3", features = [
"rt-multi-thread",
"macros",
"time",
@@ -88,7 +88,7 @@ rust_iso3166 = "0.2.0"
smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" }
serde_json = { version = "1.0.151" }
serde_json = { version = "1.0.154" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.2" }
socket2 = "0.6.5"
+4 -2
View File
@@ -3,8 +3,9 @@
<details>
<summary><strong> 🐞 修复问题 </strong></summary>
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
- 修复「首页」网站测速失败后卡片一直显示测试中的问题
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置
- 修复 首页 网站测速失败后卡片一直显示测试中
- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新
**🐧 Linux**
@@ -21,6 +22,7 @@
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 优化「链式代理」:开启前需确认网速影响提示,空面板新增「关闭」按钮
**🖥️ Windows**
@@ -15,7 +15,7 @@ serde = { workspace = true, features = ["derive"] }
sysinfo = { version = "0.39.6", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.190"
libc = "0.2.191"
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
+6 -6
View File
@@ -75,11 +75,11 @@
"monaco-yaml": "^5.5.1",
"nanoid": "^6.0.1",
"qrcode.react": "^4.2.0",
"react": "19.3.0",
"react-dom": "19.3.0",
"react": "19.3.1",
"react-dom": "19.3.1",
"react-error-boundary": "6.1.6",
"react-hook-form": "^7.88.0",
"react-i18next": "17.0.15",
"react-i18next": "17.0.19",
"react-markdown": "10.1.0",
"react-router": "^8.4.0",
"rehype-raw": "^7.0.0",
@@ -123,7 +123,7 @@
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.39.0",
"knip": "6.41.0",
"lint-staged": "^17.5.1",
"sass": "^1.104.1",
"tar": "^7.5.22",
@@ -133,7 +133,7 @@
"vite": "^8.3.0",
"vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.1",
"webdriverio": "10.0.0",
"webdriverio": "10.0.2",
"ws": "8.22.0"
},
"lint-staged": {
@@ -146,5 +146,5 @@
]
},
"type": "module",
"packageManager": "pnpm@12.11.2"
"packageManager": "pnpm@12.14.0"
}
+313 -309
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -45,6 +45,17 @@
"matchManagers": ["npm"],
"groupName": "npm dependencies"
},
{
"description": "Update Tauri npm packages and Cargo crates together so frontend and backend stay in sync",
"matchPackageNames": [
"@tauri-apps/**",
"@wdio/tauri-service",
"tauri",
"tauri-*"
],
"groupName": "tauri",
"addLabels": ["tauri"]
},
{
"description": "Group all GitHub Actions updates into a single PR",
"matchManagers": ["github-actions"],
+1 -1
View File
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.99.0"
channel = "1.99.1"
components = ["rustfmt", "clippy"]
+6 -6
View File
@@ -79,13 +79,13 @@ tauri-plugin-fs = "2.6.0"
tauri-plugin-process = "2.4.0"
tauri-plugin-deep-link = "2.6.1"
tauri-plugin-window-state = "2.5.0"
zip = "9.0.1"
zip = "9.0.4"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
base64 = "0.23.1"
minisign-verify = "0.3.0"
sha2 = "0.11"
sha2 = "0.11.1"
getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
@@ -96,24 +96,24 @@ backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.0"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.2.2", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
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.6", version = "2.7.6", features = [
"client",
] }
arc-swap = "1.9.2"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.45", features = ["ring"] }
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.9"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.19", features = ["time"] }
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"
[target.'cfg(unix)'.dependencies]
libc = "0.2.190"
libc = "0.2.191"
[target.'cfg(target_os = "linux")'.dependencies]
# Upstream has no session-end signal support on Linux; see crates/clash-verge-signal.
+6 -4
View File
@@ -282,7 +282,9 @@ async fn handle_validation_failure(
) -> CmdResult<ValidationOutcome> {
logging!(warn, Type::Cmd, "配置验证失败: {}", outcome);
discard_and_restore(current_profile).await?;
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
if !matches!(outcome, ValidationOutcome::Busy) {
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
}
Ok(outcome)
}
@@ -311,9 +313,6 @@ async fn perform_config_update(
current_value: Option<&String>,
current_profile: Option<&String>,
) -> CmdResult<ValidationOutcome> {
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let update_result = run_profile_config_update_transition(|| CoreManager::global().update_config_forced()).await;
match update_result {
@@ -409,6 +408,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(ValidationOutcome::Busy);
}
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
}
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = IProfiles {
+7
View File
@@ -75,6 +75,7 @@ interface ProxyChainProps {
onMarkUnsavedChanges?: () => void
mode?: string
selectedGroup?: string | null
onExitChainMode: () => void
}
interface ProxyChainItemProps {
@@ -331,6 +332,7 @@ export const ProxyChain = ({
onMarkUnsavedChanges,
mode,
selectedGroup,
onExitChainMode,
}: ProxyChainProps) => {
const theme = useTheme()
const { t } = useTranslation()
@@ -696,13 +698,18 @@ export const ProxyChain = ({
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 2,
height: '100%',
color: theme.palette.text.secondary,
}}
>
<Typography>{t('proxies.page.chain.empty')}</Typography>
<Button size="small" variant="outlined" onClick={onExitChainMode}>
{t('shared.actions.close')}
</Button>
</Box>
) : (
<DragDropProvider
@@ -77,6 +77,7 @@ interface GroupSelectMenuProps {
interface ProxyGroupsChainProps {
mode: string
chainConfigData?: string | null
onExitChainMode: () => void
availableGroups: any[]
activeSelectedGroup: string | null
showScrollTop: boolean
@@ -305,6 +306,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
const {
mode,
chainConfigData,
onExitChainMode,
availableGroups,
activeSelectedGroup,
showScrollTop,
@@ -521,6 +523,7 @@ export function ProxyGroupsChain(props: ProxyGroupsChainProps) {
chainConfigData={chainConfigData}
mode={mode}
selectedGroup={activeSelectedGroup}
onExitChainMode={onExitChainMode}
/>
</Box>
</Box>
+24 -8
View File
@@ -44,6 +44,7 @@ import {
ProxyGroupNavigator,
} from './proxy-group-navigator'
import { ProxyRender } from './proxy-render'
import type { HeadState } from './use-head-state'
import {
PROXY_GROUP_HEADER_SENSORS,
useProxyGroupHeaderLayout,
@@ -70,6 +71,7 @@ interface Props {
mode: string
isChainMode?: boolean
chainConfigData?: string | null
onExitChainMode: () => void
}
function useEmptyRenderList() {
@@ -90,11 +92,11 @@ function useProxyRenderState(
) {
const { verge } = useVerge()
const { proxyView } = useProxiesData()
const { renderList, onProxies, onHeadState } = useRenderList(
mode,
isChainMode,
activeSelectedGroup,
)
const {
renderList,
onProxies,
onHeadState: setListHeadState,
} = useRenderList(mode, isChainMode, activeSelectedGroup)
const scrollPositionKey = useMemo(
() =>
isChainMode
@@ -103,6 +105,14 @@ function useProxyRenderState(
[activeSelectedGroup, isChainMode, mode],
)
const onHeadState = useCallback(
(groupName: string, patch: Partial<HeadState>) => {
if (patch.open) onProxies()
setListHeadState(groupName, patch)
},
[onProxies, setListHeadState],
)
const timeout = verge?.default_latency_timeout || 10000
const handleCheckAll = useStableCallback(
@@ -190,8 +200,9 @@ function useProxyRenderState(
function ChainProxyGroups(props: {
mode: string
chainConfigData?: string | null
onExitChainMode: () => void
}) {
const { mode, chainConfigData } = props
const { mode, chainConfigData, onExitChainMode } = props
const { proxyView } = useProxiesData()
const [selectedGroup, setSelectedGroup] = useState<string | null>(null)
@@ -346,6 +357,7 @@ function ChainProxyGroups(props: {
<ProxyGroupsChain
mode={mode}
chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode}
availableGroups={availableGroups}
activeSelectedGroup={activeSelectedGroup}
showScrollTop={showScrollTop}
@@ -617,7 +629,7 @@ function NormalProxyGroups(props: { mode: string }) {
}
export const ProxyGroups = (props: Props) => {
const { mode, isChainMode = false, chainConfigData } = props
const { mode, isChainMode = false, chainConfigData, onExitChainMode } = props
const { profiles, isLoading: isProfilesLoading } = useProfiles()
const { isProxyViewPending } = useProxiesData()
const { isRunningModePending } = useSystemData()
@@ -639,7 +651,11 @@ export const ProxyGroups = (props: Props) => {
return <ProxyEmptyState reason={listState.reason} />
case 'render':
return isChainMode ? (
<ChainProxyGroups mode={mode} chainConfigData={chainConfigData} />
<ChainProxyGroups
mode={mode}
chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode}
/>
) : (
<NormalProxyGroups mode={mode} />
)
@@ -1,20 +1,11 @@
import Visibility from '@mui/icons-material/Visibility'
import VisibilityOff from '@mui/icons-material/VisibilityOff'
import {
TextField,
Button,
Grid,
Stack,
IconButton,
InputAdornment,
} from '@mui/material'
import { Button, Grid, Stack, TextField } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useState, useRef, memo, useEffect } from 'react'
import { memo, useEffect, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { useVerge } from '@/hooks/use-verge'
import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds'
import { createWebdavBackup, saveWebdavConfig } from '@/services/cmds'
import { mutate } from '@/services/mutate'
import { errorDetail, showNotice } from '@/services/notice-service'
import {
@@ -43,7 +34,6 @@ export const BackupConfigViewer = memo(
const { t } = useTranslation()
const { verge, mutateVerge } = useVerge()
const { webdav_url, webdav_username, webdav_password } = verge || {}
const [showPassword, setShowPassword] = useState(false)
const usernameRef = useRef<HTMLInputElement>(null)
const passwordRef = useRef<HTMLInputElement>(null)
const urlRef = useRef<HTMLInputElement>(null)
@@ -68,10 +58,6 @@ export const BackupConfigViewer = memo(
const webdavStatus = getWebdavStatus(webdavSignature)
const shouldAutoInit = webdavStatus !== 'failed'
const handleClickShowPassword = () => {
setShowPassword((prev) => !prev)
}
useEffect(() => {
if (!webdav_url || !webdav_username || !webdav_password) {
return
@@ -215,7 +201,7 @@ export const BackupConfigViewer = memo(
<Grid size={{ xs: 6 }}>
<TextField
label={t('shared.labels.password')}
type={showPassword ? 'text' : 'password'}
type="password"
variant="outlined"
size="small"
autoCorrect="off"
@@ -223,20 +209,6 @@ export const BackupConfigViewer = memo(
spellCheck="false"
inputRef={passwordRef}
{...register('password')}
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
},
}}
/>
</Grid>
</Grid>
+2 -5
View File
@@ -51,6 +51,7 @@ export const useProfiles = () => {
try {
const result = await mutate(() => patchProfilesConfig(value), {
id: 'patch-profiles-config',
errorNotice: false,
onFulfilled: (outcome) => {
if (outcome.status === 'valid') {
void setCacheData<IProfilesConfig>(profilesQueryKey, (current) =>
@@ -60,12 +61,8 @@ export const useProfiles = () => {
},
})
if (!result.ok) {
// Backend Busy keeps local state untouched, as before.
return result.value
}
const outcome = result.value
if (outcome.status !== 'valid' && outcome.status !== 'busy') {
if (outcome.status !== 'valid') {
await mutateProfiles()
}
return outcome
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Proxy-Kette konfigurieren",
"warning": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.",
"confirmEnable": "Verstanden, aktivieren",
"keepDisabled": "Deaktiviert lassen",
"empty": "Eine Proxy-Kette verringert die Netzwerkgeschwindigkeit deutlich. Wenn Sie keinen klaren Bedarf haben oder diese Konfiguration nicht verstehen, lassen Sie sie deaktiviert.",
"instruction": "Klicken Sie die Knoten nacheinander an, um sie zur Proxy-Kette hinzuzufügen",
"minimumNodes": "Eine Proxy-Kette braucht mindestens 2 Knoten",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "체인 프록시 구성",
"warning": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
"confirmEnable": "이해했습니다, 활성화",
"keepDisabled": "비활성화 유지",
"empty": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
"instruction": "프록시 체인에 추가하려면 순서대로 노드를 클릭하세요",
"minimumNodes": "체인 프록시는 최소 2개의 노드가 필요합니다",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Configuração do proxy em cadeia",
"warning": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
"confirmEnable": "Entendi, ativar",
"keepDisabled": "Manter desativado",
"empty": "O proxy em cadeia reduz significativamente a velocidade da rede. Se você não tem uma necessidade clara ou não entende esta configuração, mantenha-o desativado.",
"instruction": "Clique nos nós em ordem para adicioná-los à cadeia de proxy",
"minimumNodes": "O proxy em cadeia requer pelo menos 2 nós",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Цепочка прокси",
"warning": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
"confirmEnable": "Понятно, включить",
"keepDisabled": "Оставить выключенным",
"empty": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
"instruction": "Нажимайте на узлы по порядку, чтобы добавить их в цепочку",
"minimumNodes": "Для цепочки требуется минимум 2 узла",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Zincir Vekil Yapılandırması",
"warning": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
"confirmEnable": "Anladım, etkinleştir",
"keepDisabled": "Devre dışı kalsın",
"empty": "Zincir vekil ağ hızını belirgin biçimde düşürür. Açık bir ihtiyacınız yoksa ya da bu yapılandırmayı bilmiyorsanız devre dışı bırakın.",
"instruction": "Vekil zincirine eklemek için düğümlere sırayla tıklayın",
"minimumNodes": "Zincir vekil en az 2 düğüm gerektirir",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "Chain Proxy Config",
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"confirmEnable": "I understand, enable",
"keepDisabled": "Keep disabled",
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
"instruction": "Click nodes in order to add to proxy chain",
"minimumNodes": "Chain proxy requires at least 2 nodes",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "代理链配置",
"warning": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
"confirmEnable": "我明白,开启",
"keepDisabled": "保持关闭",
"empty": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
"instruction": "顺序点击节点添加到代理链中",
"minimumNodes": "链式代理至少需要 2 个节点",
+2
View File
@@ -70,6 +70,8 @@
"chain": {
"header": "鏈式代理設定",
"warning": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
"confirmEnable": "我明白,開啟",
"keepDisabled": "保持關閉",
"empty": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
"instruction": "依序點擊節點新增到鏈式代理中",
"minimumNodes": "鏈式代理至少需要 2 個節點",
+43 -24
View File
@@ -1,10 +1,10 @@
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
import { Box, Button, ButtonGroup } from '@mui/material'
import { Box, Button, ButtonGroup, Typography } from '@mui/material'
import { useLockFn } from 'ahooks'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BasePage, TooltipIcon } from '@/components/base'
import { BaseDialog, BasePage, TooltipIcon } from '@/components/base'
import { ProviderButton } from '@/components/proxy/provider-button'
import { ProxyGroups } from '@/components/proxy/proxy-groups'
import {
@@ -38,6 +38,7 @@ const ProxyPage = () => {
return false
}
})
const [chainConfirmOpen, setChainConfirmOpen] = useState(false)
const [chainConfigData, dispatchChainConfigData] = useReducer(
(_: string | null, action: string | null) => action,
@@ -68,32 +69,34 @@ const ProxyPage = () => {
}
})
const onToggleChainMode = useLockFn(async () => {
const newChainMode = !isChainMode
const applyChainMode = (enabled: boolean) => {
setIsChainMode(enabled)
localStorage.setItem('proxy-chain-mode-enabled', enabled.toString())
}
if (!newChainMode) {
// 退出链式代理模式时,清除链式代理配置
try {
debugLog('Exiting chain mode, clearing chain configuration')
const result = await mutate(
() => updateProxyChainConfigInRuntime(null),
{
id: 'update-proxy-chain-runtime',
errorNotice: false,
},
)
if (!result.ok) return
debugLog('Chain configuration cleared successfully')
} catch (error) {
console.error('Failed to clear chain configuration:', error)
showNotice.error(error)
return
}
const onExitChainMode = useLockFn(async () => {
// 退出链式代理模式时,清除链式代理配置
try {
debugLog('Exiting chain mode, clearing chain configuration')
const result = await mutate(() => updateProxyChainConfigInRuntime(null), {
id: 'update-proxy-chain-runtime',
errorNotice: false,
})
if (!result.ok) return
debugLog('Chain configuration cleared successfully')
} catch (error) {
console.error('Failed to clear chain configuration:', error)
showNotice.error(error)
return
}
setIsChainMode(newChainMode)
localStorage.setItem('proxy-chain-mode-enabled', newChainMode.toString())
applyChainMode(false)
})
const onToggleChainMode = () => {
if (isChainMode) onExitChainMode().catch(() => {})
else setChainConfirmOpen(true)
}
// 当开启链式代理模式时,获取配置数据
useEffect(() => {
if (!isChainMode) {
@@ -202,7 +205,23 @@ const ProxyPage = () => {
mode={curMode ?? 'rule'}
isChainMode={isChainMode}
chainConfigData={chainConfigData}
onExitChainMode={onExitChainMode}
/>
<BaseDialog
open={chainConfirmOpen}
title={t('proxies.page.actions.toggleChain')}
okBtn={t('proxies.page.chain.confirmEnable')}
cancelBtn={t('proxies.page.chain.keepDisabled')}
contentSx={{ width: { xs: 320, sm: 420 } }}
onCancel={() => setChainConfirmOpen(false)}
onClose={() => setChainConfirmOpen(false)}
onOk={() => {
setChainConfirmOpen(false)
applyChainMode(true)
}}
>
<Typography variant="body2">{chainWarning}</Typography>
</BaseDialog>
</BasePage>
)
}
+207 -216
View File
@@ -32,230 +32,221 @@ export const forgetShownStartupError = () => {
* type makes a backend status without a frontend entry a compile error (and
* contract regeneration keeps the union in sync).
*/
const handlers: Record<
NoticeStatus,
(msg: string, navigate: NavigateFunction) => void
> = {
info: (msg) => {
if (msg) showNotice.info(msg)
},
'import_sub_url::ok': (_msg, navigate) => {
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
// navigate("/profile", { state: { current: msg } });
navigate('/profile')
showNotice.success(
'shared.feedback.notifications.importSubscriptionSuccess',
)
},
'import_sub_url::error': (msg, navigate) => {
navigate('/profile')
showNotice.error(msg)
},
'set_config::error': (msg) => showNotice.error(msg),
'core_start::error': () => {
const read = ++startupErrorReads
void getCoreStartupError()
.then(async (failure) => {
if (read < settledStartupErrorRead) return
settledStartupErrorRead = read
if (!failure) {
shownStartupError = null
return
}
const window = getCurrentWindow()
const [visible, minimized] = await Promise.all([
window.isVisible(),
window.isMinimized(),
])
const shown = `${failure.kind}:${failure.detail}`
if (
read === settledStartupErrorRead &&
visible &&
!minimized &&
shownStartupError !== shown
) {
shownStartupError = shown
showNotice.error(
failure.kind === 'serviceCoreStopped'
? 'settings.feedback.errors.clash.serviceCoreStopped'
: 'settings.feedback.errors.clash.startFailed',
failure.detail,
)
}
})
.catch((error) => {
console.error('Failed to read the pending core startup error', error)
})
},
'service_core::repair_required': () => {
void takeServiceRepairNotice()
.then((pending) => {
if (pending) {
requestService({ reason: 'serviceLocationRefused' })
}
})
.catch((error) => {
console.error('Failed to read the pending service repair notice', error)
})
},
'service_core::app_data_not_owned': () => {
void takeServiceOwnerNotice()
.then((command) => {
if (command) {
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
shownOwnerNotice = showNotice.warning(
'settings.feedback.notifications.clashService.appDataNotOwned',
{ command },
0,
)
}
})
.catch((error) => {
console.error('Failed to read the pending service owner notice', error)
})
},
'service_core::sidecar_fallback': () => {
void takeServiceFallbackNotice()
.then((notice) => {
if (notice?.kind === 'coreRejected') {
showNotice.warning(
'settings.feedback.notifications.clashService.permissionFallback',
{ reason: notice.reason },
0,
)
} else if (notice?.kind === 'notAutoStarted') {
requestService({ reason: 'serviceNotAutoStarted' })
} else if (notice) {
showNotice.warning(
'settings.feedback.notifications.clashService.sidecarFallback',
)
}
})
.catch((error) => {
console.error(
'Failed to read the pending service fallback notice',
error,
)
})
},
'dns_override::auto_disabled': () => {
void takeDnsOverrideNotice()
.then((pending) => {
if (pending) {
showNotice.info('settings.modals.dns.protection.autoDisabled')
}
})
.catch((error) => {
console.error('Failed to read the pending DNS override notice', error)
})
},
'enhance::discarded_keys': () => {
void takeDiscardedKeysNotice()
.then((keys) => {
if (keys) {
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
keys,
})
}
})
.catch((error) => {
console.error('Failed to read the pending discarded keys notice', error)
})
},
'tun_mode::auto_disabled': () =>
showNotice.info(
'settings.sections.system.notifications.tunMode.autoDisabled',
),
'tun_mode::auto_disable_failed': () =>
showNotice.error(
'settings.sections.system.notifications.tunMode.autoDisableFailed',
),
'app_restart::core_stop_failed': (msg) =>
showNotice.error(
'layout.feedback.errors.restartCoreStopFailed',
msg || undefined,
),
'app_quit::core_stop_failed': (msg) =>
showNotice.error(
'layout.feedback.errors.quitCoreStopFailed',
msg || undefined,
),
update_with_clash_proxy: (msg) =>
showNotice.success(
'settings.feedback.notifications.updater.withClashProxySuccess',
msg,
),
update_failed_even_with_clash: (msg) =>
showNotice.error(
'settings.feedback.notifications.updater.withClashProxyFailed',
msg,
),
'reactivate_profiles::error': (msg) => showNotice.error(msg),
update_failed: (msg) => showNotice.error(msg),
'config_validate::boot_error': (msg) =>
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
'config_validate::error': (msg) =>
showNotice.error('shared.feedback.validation.config.failed', msg),
'config_validate::process_terminated': () =>
showNotice.error('shared.feedback.validation.config.processTerminated'),
'config_validate::script_error': (msg) =>
showNotice.error('shared.feedback.validation.script.fileError', msg),
'config_validate::script_syntax_error': (msg) =>
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
'config_validate::script_missing_main': (msg) =>
showNotice.error('shared.feedback.validation.script.missingMain', msg),
'config_validate::file_not_found': (msg) =>
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
'config_validate::yaml_syntax_error': (msg) =>
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
'config_validate::yaml_read_error': (msg) =>
showNotice.error('shared.feedback.validation.yaml.readError', msg),
'config_validate::yaml_mapping_error': (msg) =>
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
'config_validate::merge_syntax_error': (msg) =>
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
'config_validate::merge_mapping_error': (msg) =>
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
'config_core::change_success': (msg) =>
showNotice.success(
'settings.feedback.notifications.clash.changeSuccess',
msg,
),
'config_core::change_error': (msg) =>
showNotice.error('settings.feedback.notifications.clash.changeFailed', msg),
'mixed_port::fallback': (msg) => {
const [oldPort, newPort] = msg.split(',')
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
oldPort,
newPort,
})
},
'mixed_port::fallback_error': (msg) =>
showNotice.error(
'settings.modals.clashPort.messages.automaticFallbackFailed',
{ error: msg },
),
}
export const handleNoticeMessage = (
status: string,
msg: string,
t: TranslateFunction,
navigate: NavigateFunction,
) => {
const handlers: Record<NoticeStatus, () => void> = {
info: () => {
if (msg) showNotice.info(msg)
},
'import_sub_url::ok': () => {
// 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
// 未来细分事件通知时,可以考虑传入订阅 ID 或其他标识符
// navigate("/profile", { state: { current: msg } });
navigate('/profile')
showNotice.success(
'shared.feedback.notifications.importSubscriptionSuccess',
)
},
'import_sub_url::error': () => {
navigate('/profile')
showNotice.error(msg)
},
'set_config::error': () => showNotice.error(msg),
'core_start::error': () => {
const read = ++startupErrorReads
void getCoreStartupError()
.then(async (failure) => {
if (read < settledStartupErrorRead) return
settledStartupErrorRead = read
if (!failure) {
shownStartupError = null
return
}
const window = getCurrentWindow()
const [visible, minimized] = await Promise.all([
window.isVisible(),
window.isMinimized(),
])
const shown = `${failure.kind}:${failure.detail}`
if (
read === settledStartupErrorRead &&
visible &&
!minimized &&
shownStartupError !== shown
) {
shownStartupError = shown
showNotice.error(
failure.kind === 'serviceCoreStopped'
? 'settings.feedback.errors.clash.serviceCoreStopped'
: 'settings.feedback.errors.clash.startFailed',
failure.detail,
)
}
})
.catch((error) => {
console.error('Failed to read the pending core startup error', error)
})
},
'service_core::repair_required': () => {
void takeServiceRepairNotice()
.then((pending) => {
if (pending) {
requestService({ reason: 'serviceLocationRefused' })
}
})
.catch((error) => {
console.error(
'Failed to read the pending service repair notice',
error,
)
})
},
'service_core::app_data_not_owned': () => {
void takeServiceOwnerNotice()
.then((command) => {
if (command) {
if (shownOwnerNotice !== null) hideNotice(shownOwnerNotice)
shownOwnerNotice = showNotice.warning(
'settings.feedback.notifications.clashService.appDataNotOwned',
{ command },
0,
)
}
})
.catch((error) => {
console.error(
'Failed to read the pending service owner notice',
error,
)
})
},
'service_core::sidecar_fallback': () => {
void takeServiceFallbackNotice()
.then((notice) => {
if (notice?.kind === 'coreRejected') {
showNotice.warning(
'settings.feedback.notifications.clashService.permissionFallback',
{ reason: notice.reason },
0,
)
} else if (notice?.kind === 'notAutoStarted') {
requestService({ reason: 'serviceNotAutoStarted' })
} else if (notice) {
showNotice.warning(
'settings.feedback.notifications.clashService.sidecarFallback',
)
}
})
.catch((error) => {
console.error(
'Failed to read the pending service fallback notice',
error,
)
})
},
'dns_override::auto_disabled': () => {
void takeDnsOverrideNotice()
.then((pending) => {
if (pending) {
showNotice.info('settings.modals.dns.protection.autoDisabled')
}
})
.catch((error) => {
console.error('Failed to read the pending DNS override notice', error)
})
},
'enhance::discarded_keys': () => {
void takeDiscardedKeysNotice()
.then((keys) => {
if (keys) {
showNotice.warning('profiles.page.feedback.notices.discardedKeys', {
keys,
})
}
})
.catch((error) => {
console.error(
'Failed to read the pending discarded keys notice',
error,
)
})
},
'tun_mode::auto_disabled': () =>
showNotice.info(
'settings.sections.system.notifications.tunMode.autoDisabled',
),
'tun_mode::auto_disable_failed': () =>
showNotice.error(
'settings.sections.system.notifications.tunMode.autoDisableFailed',
),
'app_restart::core_stop_failed': () =>
showNotice.error(
'layout.feedback.errors.restartCoreStopFailed',
msg || undefined,
),
'app_quit::core_stop_failed': () =>
showNotice.error(
'layout.feedback.errors.quitCoreStopFailed',
msg || undefined,
),
update_with_clash_proxy: () =>
showNotice.success(
'settings.feedback.notifications.updater.withClashProxySuccess',
msg,
),
update_failed_even_with_clash: () =>
showNotice.error(
'settings.feedback.notifications.updater.withClashProxyFailed',
msg,
),
'reactivate_profiles::error': () => showNotice.error(msg),
update_failed: () => showNotice.error(msg),
'config_validate::boot_error': () =>
showNotice.error('shared.feedback.validation.config.bootFailed', msg),
'config_validate::error': () =>
showNotice.error('shared.feedback.validation.config.failed', msg),
'config_validate::process_terminated': () =>
showNotice.error('shared.feedback.validation.config.processTerminated'),
'config_validate::script_error': () =>
showNotice.error('shared.feedback.validation.script.fileError', msg),
'config_validate::script_syntax_error': () =>
showNotice.error('shared.feedback.validation.script.syntaxError', msg),
'config_validate::script_missing_main': () =>
showNotice.error('shared.feedback.validation.script.missingMain', msg),
'config_validate::file_not_found': () =>
showNotice.error('shared.feedback.validation.script.fileNotFound', msg),
'config_validate::yaml_syntax_error': () =>
showNotice.error('shared.feedback.validation.yaml.syntaxError', msg),
'config_validate::yaml_read_error': () =>
showNotice.error('shared.feedback.validation.yaml.readError', msg),
'config_validate::yaml_mapping_error': () =>
showNotice.error('shared.feedback.validation.yaml.mappingError', msg),
'config_validate::merge_syntax_error': () =>
showNotice.error('shared.feedback.validation.merge.syntaxError', msg),
'config_validate::merge_mapping_error': () =>
showNotice.error('shared.feedback.validation.merge.mappingError', msg),
'config_core::change_success': () =>
showNotice.success(
'settings.feedback.notifications.clash.changeSuccess',
msg,
),
'config_core::change_error': () =>
showNotice.error(
'settings.feedback.notifications.clash.changeFailed',
msg,
),
'mixed_port::fallback': () => {
const [oldPort, newPort] = msg.split(',')
showNotice.info('settings.modals.clashPort.messages.automaticFallback', {
oldPort,
newPort,
})
},
'mixed_port::fallback_error': () =>
showNotice.error(
'settings.modals.clashPort.messages.automaticFallbackFailed',
{ error: msg },
),
}
const handler = handlers[status as NoticeStatus]
if (handler) {
handler()
handler(msg, navigate)
} else {
console.warn(`未处理的通知状态: ${status}`)
}
+1
View File
@@ -66,6 +66,7 @@ const withLoading = (
next.delete(uid)
}
}
if (next.size === state.profileUpdates.loading.size) return state
return {
...state,
profileUpdates: { ...state.profileUpdates, loading: next },
+2
View File
@@ -322,6 +322,8 @@ export const translationKeys = [
'proxies.page.empty.actions.openLogs',
'proxies.page.chain.header',
'proxies.page.chain.warning',
'proxies.page.chain.confirmEnable',
'proxies.page.chain.keepDisabled',
'proxies.page.chain.empty',
'proxies.page.chain.instruction',
'proxies.page.chain.minimumNodes',
+2
View File
@@ -525,6 +525,7 @@ export interface TranslationResources {
toggleChain: string
}
chain: {
confirmEnable: string
connectFailed: string
disconnectFailed: string
duplicateNode: string
@@ -533,6 +534,7 @@ export interface TranslationResources {
exitNode: string
header: string
instruction: string
keepDisabled: string
minimumNodes: string
minimumNodesHint: string
warning: string