mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
Compare commits
112
Commits
83a06e5716
...
71429f9a42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71429f9a42 | ||
|
|
6e08a77a84 | ||
|
|
27efaf34f4 | ||
|
|
585c67f8c5 | ||
|
|
d6a2717fc4 | ||
|
|
84312a5cd1 | ||
|
|
63ee674dcb | ||
|
|
52dca35acb | ||
|
|
44477d97d4 | ||
|
|
0fefa7a7ff | ||
|
|
b6d94d45d5 | ||
|
|
9975322b9b | ||
|
|
4337bd8575 | ||
|
|
cb751052a4 | ||
|
|
d9dc18a57b | ||
|
|
6b4d4e1551 | ||
|
|
4a125681bd | ||
|
|
1c2817023e | ||
|
|
89076f01c3 | ||
|
|
a4531cf926 | ||
|
|
07b0e71cc6 | ||
|
|
ad2baf254a | ||
|
|
d01f43cc78 | ||
|
|
720dc13e96 | ||
|
|
69b76cc6a9 | ||
|
|
8328e34208 | ||
|
|
78691ea5c8 | ||
|
|
af808a677c | ||
|
|
a1bebf6de1 | ||
|
|
2caff762b5 | ||
|
|
dba0f471bf | ||
|
|
c341b9b238 | ||
|
|
6ec35c8195 | ||
|
|
30f05a7a39 | ||
|
|
7cc6c1ed95 | ||
|
|
c7cee50fc1 | ||
|
|
4cae1ff132 | ||
|
|
1a01844cb8 | ||
|
|
1575f134e0 | ||
|
|
a9a3cfa3a7 | ||
|
|
14770df157 | ||
|
|
57c5484080 | ||
|
|
4a0882fc18 | ||
|
|
d87bb7b6a3 | ||
|
|
a58cefdc31 | ||
|
|
f958dccb25 | ||
|
|
8c1fb3ac56 | ||
|
|
3607e66bb3 | ||
|
|
ea509b8236 | ||
|
|
52d616302c | ||
|
|
8ddd99f226 | ||
|
|
e0e97a920e | ||
|
|
c4f9d65e07 | ||
|
|
0c7e0c8535 | ||
|
|
1638ab48d8 | ||
|
|
5d7dc3aed9 | ||
|
|
b057bd964c | ||
|
|
897a117dc5 | ||
|
|
6a752994cf | ||
|
|
32e5d5bda3 | ||
|
|
daae17a40c | ||
|
|
1f140dfd3a | ||
|
|
04d74ac48f | ||
|
|
78bf79cc09 | ||
|
|
61c33c6113 | ||
|
|
ae4f416766 | ||
|
|
fbc4b516b8 | ||
|
|
3456de1fb6 | ||
|
|
f9ef9d78cb | ||
|
|
3458c9b075 | ||
|
|
afc32f568a | ||
|
|
22e3f1ac8a | ||
|
|
758300ac34 | ||
|
|
659ee1a181 | ||
|
|
fd06fbb3b7 | ||
|
|
ed0b801573 | ||
|
|
ff1ce92fac | ||
|
|
6a85d0344e | ||
|
|
bffe91db5e | ||
|
|
93b7bb8fe8 | ||
|
|
3b958acb22 | ||
|
|
d374ab4350 | ||
|
|
b7197f6f4b | ||
|
|
7bf8e1dcf5 | ||
|
|
07825afb1e | ||
|
|
d7e0126586 | ||
|
|
5059cdcb9d | ||
|
|
af0f502f98 | ||
|
|
8982525fd2 | ||
|
|
81ce247ecc | ||
|
|
b469bee352 | ||
|
|
df79ed3efb | ||
|
|
c6a4de1c6a | ||
|
|
433cd0628f | ||
|
|
13676d3335 | ||
|
|
2a7972fa5b | ||
|
|
19334368cd | ||
|
|
73ea0fbd80 | ||
|
|
9f97d938f5 | ||
|
|
04a2a09368 | ||
|
|
94491b4ba4 | ||
|
|
78516dc1dc | ||
|
|
8f6fd3bbf9 | ||
|
|
c599f5547e | ||
|
|
f6fdb34745 | ||
|
|
65ed8d3cf2 | ||
|
|
2baa629ac4 | ||
|
|
c7ffb21294 | ||
|
|
0d0ed72467 | ||
|
|
a36dcd319f | ||
|
|
9a879dd8d8 | ||
|
|
20141e791e |
@@ -5,10 +5,10 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:2": {
|
"ghcr.io/devcontainers/features/node:2": {
|
||||||
"version": "24.21.0",
|
"version": "24.21.0",
|
||||||
"pnpmVersion": "12.3.3"
|
"pnpmVersion": "12.11.2"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/rust:1": {
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
"version": "1.98.1",
|
"version": "1.99.0",
|
||||||
"profile": "default"
|
"profile": "default"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/git:1": {},
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "v9.0.0",
|
"version": "v9.0.0",
|
||||||
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
|
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
|
||||||
},
|
},
|
||||||
"github/gh-aw-actions/setup@v0.89.1": {
|
"github/gh-aw-actions/setup@v0.89.21": {
|
||||||
"repo": "github/gh-aw-actions/setup",
|
"repo": "github/gh-aw-actions/setup",
|
||||||
"version": "v0.89.1",
|
"version": "v0.89.21",
|
||||||
"sha": "4537e5924c9abb366dcbece06750e498e0218fae"
|
"sha": "924af5fdc64061cfbf66fb584c8b07e2ac230c60"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"containers": {
|
"containers": {
|
||||||
@@ -22,6 +22,16 @@
|
|||||||
"digest": "sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98",
|
"digest": "sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98",
|
||||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.14@sha256:f7df036c86575527b61f3f7df91c4412349a12b2a74988d929eafa2999230c98"
|
||||||
},
|
},
|
||||||
|
"ghcr.io/github/gh-aw-firewall/agent:0.28.16": {
|
||||||
|
"image": "ghcr.io/github/gh-aw-firewall/agent:0.28.16",
|
||||||
|
"digest": "sha256:57a3e27388a6d7d32719088581e52567727fa0bf2f0d477bf565b0c4baa12a3f",
|
||||||
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.16@sha256:57a3e27388a6d7d32719088581e52567727fa0bf2f0d477bf565b0c4baa12a3f"
|
||||||
|
},
|
||||||
|
"ghcr.io/github/gh-aw-firewall/agent:0.28.23": {
|
||||||
|
"image": "ghcr.io/github/gh-aw-firewall/agent:0.28.23",
|
||||||
|
"digest": "sha256:2c78aaba1c108e130e2d6d01e4f2cca334ea04c53e6f258913ac34173fe7e3b2",
|
||||||
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.28.23@sha256:2c78aaba1c108e130e2d6d01e4f2cca334ea04c53e6f258913ac34173fe7e3b2"
|
||||||
|
},
|
||||||
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13": {
|
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13": {
|
||||||
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13",
|
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.13",
|
||||||
"digest": "sha256:61f12dcff38eb008b645d87a5541763452f409d9040eb243718764944167b1a2",
|
"digest": "sha256:61f12dcff38eb008b645d87a5541763452f409d9040eb243718764944167b1a2",
|
||||||
@@ -32,6 +42,16 @@
|
|||||||
"digest": "sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5",
|
"digest": "sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5",
|
||||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.14@sha256:6f95e2234dd9bd6333a8ff28ccea7ecf0204acd4a09108723844dbd2bf6268c5"
|
||||||
},
|
},
|
||||||
|
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16": {
|
||||||
|
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16",
|
||||||
|
"digest": "sha256:cd400948638ffe1b87ec319abf73fa29b6ac9881b015da58bba971c4cc13a400",
|
||||||
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.16@sha256:cd400948638ffe1b87ec319abf73fa29b6ac9881b015da58bba971c4cc13a400"
|
||||||
|
},
|
||||||
|
"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.23": {
|
||||||
|
"image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.23",
|
||||||
|
"digest": "sha256:c15c3d1208df10c5b588a3657be53742aa982ae0909d1eb1812525268794ca64",
|
||||||
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.28.23@sha256:c15c3d1208df10c5b588a3657be53742aa982ae0909d1eb1812525268794ca64"
|
||||||
|
},
|
||||||
"ghcr.io/github/gh-aw-firewall/squid:0.28.13": {
|
"ghcr.io/github/gh-aw-firewall/squid:0.28.13": {
|
||||||
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.13",
|
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.13",
|
||||||
"digest": "sha256:58a8dc5be3fbafeef7da0532c38bd9047f86bf1b3c4cfd7d9b7d8d28e80a2b56",
|
"digest": "sha256:58a8dc5be3fbafeef7da0532c38bd9047f86bf1b3c4cfd7d9b7d8d28e80a2b56",
|
||||||
@@ -42,20 +62,30 @@
|
|||||||
"digest": "sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5",
|
"digest": "sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5",
|
||||||
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.14@sha256:2ce8df3abf3e9b76e9c0cf5863da41f1ab3f89b20ad14b988806ab89e7bf2cd5"
|
||||||
},
|
},
|
||||||
"ghcr.io/github/gh-aw-mcpg:v0.4.20": {
|
"ghcr.io/github/gh-aw-firewall/squid:0.28.16": {
|
||||||
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.20",
|
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.16",
|
||||||
"digest": "sha256:980ea7aa4fb07e444f0e2c6e3af5aff8b45e4e415d617667b8ba064a1768b684",
|
"digest": "sha256:452197f2e241b2cda8eb0b5674960aa8ca544a64dc242e24f8263c3df6452919",
|
||||||
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.20@sha256:980ea7aa4fb07e444f0e2c6e3af5aff8b45e4e415d617667b8ba064a1768b684"
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.16@sha256:452197f2e241b2cda8eb0b5674960aa8ca544a64dc242e24f8263c3df6452919"
|
||||||
|
},
|
||||||
|
"ghcr.io/github/gh-aw-firewall/squid:0.28.23": {
|
||||||
|
"image": "ghcr.io/github/gh-aw-firewall/squid:0.28.23",
|
||||||
|
"digest": "sha256:02ffc56dd40158223064ef03a78c2d9717473c93723b4e403d455ea6f0b09ae0",
|
||||||
|
"pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.28.23@sha256:02ffc56dd40158223064ef03a78c2d9717473c93723b4e403d455ea6f0b09ae0"
|
||||||
|
},
|
||||||
|
"ghcr.io/github/gh-aw-mcpg:v0.4.25": {
|
||||||
|
"image": "ghcr.io/github/gh-aw-mcpg:v0.4.25",
|
||||||
|
"digest": "sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086",
|
||||||
|
"pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.4.25@sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086"
|
||||||
},
|
},
|
||||||
"ghcr.io/github/gh-aw-node": {
|
"ghcr.io/github/gh-aw-node": {
|
||||||
"image": "ghcr.io/github/gh-aw-node",
|
"image": "ghcr.io/github/gh-aw-node",
|
||||||
"digest": "sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e",
|
"digest": "sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e",
|
||||||
"pinned_image": "ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"
|
"pinned_image": "ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"
|
||||||
},
|
},
|
||||||
"ghcr.io/github/github-mcp-server:v1.12.1": {
|
"ghcr.io/github/github-mcp-server:v1.12.2": {
|
||||||
"image": "ghcr.io/github/github-mcp-server:v1.12.1",
|
"image": "ghcr.io/github/github-mcp-server:v1.12.2",
|
||||||
"digest": "sha256:0ba840c46a237879c8300e7fddb0b6347f20e029ccb9cbe2ce4a943daa1ff560",
|
"digest": "sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6",
|
||||||
"pinned_image": "ghcr.io/github/github-mcp-server:v1.12.1@sha256:0ba840c46a237879c8300e7fddb0b6347f20e029ccb9cbe2ce4a943daa1ff560"
|
"pinned_image": "ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ name: Auto Build
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# UTC+8 12:00, 18:00 -> UTC 4:00, 10:00
|
# UTC+8 06:00, 18:00 -> UTC 22:00, 10:00
|
||||||
- cron: '0 4,10 * * *'
|
- cron: '0 10,22 * * *'
|
||||||
permissions: write-all
|
permissions: write-all
|
||||||
env:
|
env:
|
||||||
TAG_NAME: autobuild
|
TAG_NAME: autobuild
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
### 稳定机场VPN推荐
|
### 稳定机场VPN推荐
|
||||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- [狗狗加速](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
@@ -154,7 +154,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.98.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -206,7 +206,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.98.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -347,7 +347,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -580,7 +580,7 @@ jobs:
|
|||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
### 稳定机场VPN推荐
|
### 稳定机场VPN推荐
|
||||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- [狗狗加速](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Backend Guardrails
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src-tauri/**"
|
||||||
|
- ".github/workflows/backend-guardrails.yml"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
banned-patterns:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: No Handle::refresh calls in feat
|
||||||
|
run: '! grep -rn "Handle::refresh" src-tauri/src/feat'
|
||||||
|
|
||||||
|
- name: No notice_message outside core/notify
|
||||||
|
run: '! grep -rn "notice_message(" src-tauri/src --include="*.rs" | grep -v "core/notify"'
|
||||||
|
|
||||||
|
- name: No manual update-flag chains
|
||||||
|
run: '! grep -rnE "determine_update_flags|UpdateFlags" src-tauri/src'
|
||||||
@@ -31,6 +31,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Install gh-aw extension
|
- name: Install gh-aw extension
|
||||||
uses: github/gh-aw-actions/setup-cli@045beb2d14bda8d0c1f2e83b41527f63770d2855 # v0.89.15
|
uses: github/gh-aw-actions/setup-cli@924af5fdc64061cfbf66fb584c8b07e2ac230c60 # v0.89.21
|
||||||
with:
|
with:
|
||||||
version: v0.89.1
|
version: v0.89.15
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: dtolnay/rust-toolchain@1.98.1
|
uses: dtolnay/rust-toolchain@1.99.0
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
run: |
|
run: |
|
||||||
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
HUSKY: 0
|
HUSKY: 0
|
||||||
@@ -85,3 +86,73 @@ jobs:
|
|||||||
- name: Run Knip
|
- name: Run Knip
|
||||||
if: steps.check_frontend.outputs.frontend == 'true'
|
if: steps.check_frontend.outputs.frontend == 'true'
|
||||||
run: pnpm knip:check
|
run: pnpm knip:check
|
||||||
|
|
||||||
|
- name: Architecture gates (listen/emit only in the event bus, invoke only in services)
|
||||||
|
if: steps.check_frontend.outputs.frontend == 'true'
|
||||||
|
run: node scripts/check-architecture.mjs listen emit invoke
|
||||||
|
|
||||||
|
contract-parity:
|
||||||
|
runs-on: ubuntu-26.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Check contract inputs
|
||||||
|
id: check_inputs
|
||||||
|
uses: dorny/paths-filter@v4
|
||||||
|
with:
|
||||||
|
filters: scripts/contract-inputs.json
|
||||||
|
|
||||||
|
- name: Skip if contract inputs unchanged
|
||||||
|
if: steps.check_inputs.outputs.contract != 'true'
|
||||||
|
run: echo "No contract inputs changed, skipping parity check."
|
||||||
|
|
||||||
|
- name: Install Rust Stable
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
prefix-key: 'v1-rust'
|
||||||
|
key: 'rust-shared-stable-ubuntu-22.04-x86_64-unknown-linux-gnu-contract'
|
||||||
|
workspaces: |
|
||||||
|
. -> target
|
||||||
|
cache-all-crates: true
|
||||||
|
cache-workspace-crates: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxslt1.1 libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
with:
|
||||||
|
node-version: '24.21.0'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
|
||||||
|
- name: Provision sidecar placeholders for tauri-build
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
run: |
|
||||||
|
mkdir -p src-tauri/sidecar src-tauri/resources dist
|
||||||
|
touch dist/index.html
|
||||||
|
for bin in clash-verge-service clash-verge-service-install clash-verge-service-uninstall verge-mihomo verge-mihomo-alpha; do
|
||||||
|
touch "src-tauri/sidecar/${bin}-x86_64-unknown-linux-gnu"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Check generated contract without changing the checkout
|
||||||
|
if: steps.check_inputs.outputs.contract == 'true'
|
||||||
|
run: pnpm gen:contract --check
|
||||||
|
|||||||
+172
-128
File diff suppressed because one or more lines are too long
@@ -41,13 +41,17 @@ pre-agent-steps:
|
|||||||
' }',
|
' }',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
const nonBlockingQuotaBranch = [
|
const nonBlockingQuotaBranch = [
|
||||||
|
' let quotaErrorMsg = null;',
|
||||||
' if (isQuotaExceeded) {',
|
' if (isQuotaExceeded) {',
|
||||||
' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',
|
' log(`attempt ${attempt + 1}: Copilot quota exceeded — not retrying`);',
|
||||||
' if (/\\b429\\b|Too Many Requests/i.test(result.output)) {',
|
' quotaErrorMsg = { action: "stop" };',
|
||||||
' log(`attempt ${attempt + 1}: Copilot quota unavailable — exiting 0`);',
|
' }',
|
||||||
' return { action: "stop", exitCode: 0 };',
|
' if (/\\b429\\b|Too Many Requests/i.test(result.output)) {',
|
||||||
' }',
|
' log(`attempt ${attempt + 1}: Copilot quota unavailable — exiting 0`);',
|
||||||
' return { action: "stop" };',
|
' quotaErrorMsg = { action: "stop", exitCode: 0 };',
|
||||||
|
' }',
|
||||||
|
' if (quotaErrorMsg) {',
|
||||||
|
' return quotaErrorMsg;',
|
||||||
' }',
|
' }',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ jobs:
|
|||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
### 稳定机场VPN推荐
|
### 稳定机场VPN推荐
|
||||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- [狗狗加速](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
@@ -204,7 +204,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.98.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -244,7 +244,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.98.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -389,7 +389,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ jobs:
|
|||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: '1.98.1'
|
toolchain: '1.99.0'
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
@@ -487,7 +487,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Add Rust Target
|
- name: Add Rust Target
|
||||||
run: |
|
run: |
|
||||||
rustup target add ${{ matrix.target }} --toolchain 1.98.1 || rustup target add ${{ matrix.target }}
|
rustup target add ${{ matrix.target }} --toolchain 1.99.0 || rustup target add ${{ matrix.target }}
|
||||||
rustup target list --installed
|
rustup target list --installed
|
||||||
echo "Rust target ${{ matrix.target }} installed."
|
echo "Rust target ${{ matrix.target }} installed."
|
||||||
|
|
||||||
@@ -691,7 +691,7 @@ jobs:
|
|||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
### 稳定机场VPN推荐
|
### 稳定机场VPN推荐
|
||||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- [狗狗加速](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
name: SignPath Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
HUSKY: 0
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sign-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 90
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: '1.99.0'
|
||||||
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: '24.21.0'
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Prepare build resources
|
||||||
|
run: pnpm run prebuild x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Build test installer
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
|
run: |
|
||||||
|
$configPath = Join-Path $env:RUNNER_TEMP 'signpath-test.json'
|
||||||
|
'{"bundle":{"createUpdaterArtifacts":false}}' |
|
||||||
|
Set-Content -LiteralPath $configPath -Encoding utf8
|
||||||
|
pnpm exec tauri build --target x86_64-pc-windows-msvc --bundles nsis --config $configPath
|
||||||
|
|
||||||
|
- name: Upload unsigned installer
|
||||||
|
id: unsigned
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: windows-x64-unsigned
|
||||||
|
path: target/x86_64-pc-windows-msvc/release/bundle/nsis/*-setup.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Sign with SignPath test certificate
|
||||||
|
uses: signpath/github-action-submit-signing-request@v3
|
||||||
|
with:
|
||||||
|
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||||
|
organization-id: '5b298f4d-010d-4bae-b665-70551a58839c'
|
||||||
|
project-slug: 'clash-verge-rev'
|
||||||
|
signing-policy-slug: 'test-signing'
|
||||||
|
artifact-configuration-slug: 'initial'
|
||||||
|
github-artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
|
||||||
|
wait-for-completion: true
|
||||||
|
output-artifact-directory: signed
|
||||||
|
|
||||||
|
- name: Inspect installer signature
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Get-ChildItem -Path signed/*-setup.exe | ForEach-Object {
|
||||||
|
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
|
||||||
|
if (-not $signature.SignerCertificate) {
|
||||||
|
throw "No Authenticode signer certificate found for $($_.Name)"
|
||||||
|
}
|
||||||
|
$signature | Format-List Path, Status, StatusMessage, SignerCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload signed test installer
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: windows-x64-test-signed
|
||||||
|
path: signed/*-setup.exe
|
||||||
|
if-no-files-found: error
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|
||||||
### 稳定机场VPN推荐
|
### 稳定机场VPN推荐
|
||||||
- [狗狗加速](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- [狗狗加速](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
Created at ${{ env.BUILDTIME }}.
|
Created at ${{ env.BUILDTIME }}.
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ if ! command -v "cargo-make" >/dev/null 2>&1; then
|
|||||||
cargo install --force cargo-make
|
cargo install --force cargo-make
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
node scripts/check-contract-changes.mjs --push "$1" || exit $?
|
||||||
cargo make pre-push
|
cargo make pre-push
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
nodejs 24.21.0
|
nodejs 24.21.0
|
||||||
pnpm 12.4.1
|
pnpm 12.11.2
|
||||||
rust 1.98.1
|
rust 1.99.0
|
||||||
|
|||||||
Generated
+427
-488
File diff suppressed because it is too large
Load Diff
+16
-8
@@ -47,11 +47,17 @@ codegen-units = 1
|
|||||||
split-debuginfo = "unpacked"
|
split-debuginfo = "unpacked"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# macOS Golden Gate fix for tray-icon click reentrancy issue
|
# Backport Windows menu command ID recycling on the muda 0.20 compatibility branch.
|
||||||
# Remove the following patch once the fix is merged upstream(tauri)
|
muda = { git = "https://github.com/wonfen/muda.git", rev = "547de49352791653fbdfa785f20f3c7ece8468e2" }
|
||||||
# https://github.com/tauri-apps/tray-icon/pull/365
|
# tauri-specta has no tauri 3 release yet; ride the tauri team's port and the
|
||||||
tray-icon = { git = "https://github.com/Tunglies/tray-icon.git", branch = "fix/macos-27-click-reentrancy" }
|
# unpublished specta rc.26 line it pins. Drop all six entries once both ship
|
||||||
|
# on crates.io.
|
||||||
|
tauri-specta = { git = "https://github.com/tauri-apps/tauri-specta", branch = "tauri-v3" }
|
||||||
|
specta = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||||
|
specta-macros = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||||
|
specta-serde = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||||
|
specta-typescript = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||||
|
specta-util = { git = "https://github.com/specta-rs/specta.git", rev = "2757fa8899926dce7da999780c7edf2dc08a732a" }
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
clash-verge-draft = { path = "crates/clash-verge-draft" }
|
||||||
clash-verge-media-unlock = { path = "crates/clash-verge-media-unlock" }
|
clash-verge-media-unlock = { path = "crates/clash-verge-media-unlock" }
|
||||||
@@ -63,13 +69,14 @@ tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", bra
|
|||||||
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
|
||||||
tauri = { version = "2.11.5" }
|
tauri = { version = "3.0.0-alpha.4" }
|
||||||
tauri-plugin-clipboard-manager = "2.3.3"
|
tauri-runtime-wry = "3.0.0-alpha.4"
|
||||||
|
tauri-plugin-clipboard-manager = "3.0.0-alpha.2"
|
||||||
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
|
||||||
anyhow = "1.0.104"
|
anyhow = "1.0.104"
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||||
tokio = { version = "1.53.1", features = [
|
tokio = { version = "1.53.2", features = [
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"macros",
|
"macros",
|
||||||
"time",
|
"time",
|
||||||
@@ -105,6 +112,7 @@ windows-sys = { version = "0.61.2", features = [
|
|||||||
"Win32_Security_Authorization",
|
"Win32_Security_Authorization",
|
||||||
"Win32_Storage_FileSystem",
|
"Win32_Storage_FileSystem",
|
||||||
"Win32_System_Threading",
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_Diagnostics_ToolHelp",
|
||||||
"Win32_System_JobObjects",
|
"Win32_System_JobObjects",
|
||||||
] }
|
] }
|
||||||
# *********************************
|
# *********************************
|
||||||
|
|||||||
+3
-82
@@ -1,103 +1,24 @@
|
|||||||
## v2.5.4
|
## v2.5.9
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> 服务模式下,配置目录中的规则集、代理集合文件只是缓存副本,手动修改不会影响正在运行的内核。需要手动维护的文件请使用 `type: file`。
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
- 修复系统代理切换失败后,开关状态显示错误的问题
|
- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置的问题
|
||||||
- 修复代理守卫失败后反复重试且没有提示的问题
|
|
||||||
- 修复未开启系统代理时,启动仍提示代理错误的问题
|
|
||||||
- 修复恢复备份后未按备份设置关闭系统代理的问题
|
|
||||||
- 修复服务模式下操作超时后,后续操作报错或状态异常的问题
|
|
||||||
- 修复部分节点显示「Unknown」或无法测试延迟的问题
|
|
||||||
- 修复 `PASS-RULE` 节点被错误隐藏的问题
|
|
||||||
- 修复重启内核后未恢复上次所选节点的问题
|
|
||||||
- 修复部分代理组和首页图标无法显示、页面报错的问题
|
|
||||||
- 修复同时修改端口、TUN 等设置时,部分修改丢失的问题
|
|
||||||
- 修复 TUN 模式下 IPv6 Fake-IP 默认地址范围错误的问题
|
|
||||||
- 修复切换节点时应用可能卡住的问题
|
|
||||||
- 修复订阅文件被误删、删除或排序后列表异常的问题
|
|
||||||
- 修复订阅自动更新失败无提示、重启后更新时间延后及状态误报的问题
|
|
||||||
- 修复订阅编辑器因节点名称为空而崩溃、因格式错误而丢失内容的问题
|
|
||||||
- 修复 DNS 编辑器空内容报错、初始值覆盖已有设置的问题
|
|
||||||
- 修复关闭自动检查更新后,手动检查更新无效的问题
|
|
||||||
- 修复应用冷启动或从轻量模式打开界面时, 系统代理被重复设置的问题
|
|
||||||
|
|
||||||
**🖥️ Windows**
|
|
||||||
|
|
||||||
- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题
|
|
||||||
- 修复 Windows 系统代理设置偶发失败的问题
|
|
||||||
- 修复 Windows 关机被应用阻止的问题
|
|
||||||
- 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加
|
|
||||||
|
|
||||||
**🍎 macOS**
|
|
||||||
|
|
||||||
- 修复 macOS 内核更新失败后无法启动的问题
|
|
||||||
- 修复 macOS 需要管理员授权或开机自启时,系统代理设置失败的问题
|
|
||||||
- 修复 macOS 切换显示器后托盘速率样式不同步的问题
|
|
||||||
|
|
||||||
**🐧 Linux**
|
|
||||||
|
|
||||||
- 修复 Linux 安装或卸载服务时授权失败的问题
|
|
||||||
|
|
||||||
**🍎/🐧 macOS/Linux**
|
|
||||||
|
|
||||||
- 修复 macOS/Linux 系统代理绕过规则未正确生效的问题
|
|
||||||
- 修复 macOS/Linux 因旧版服务缓存权限异常而无法记住节点的问题
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
- 新增混合代理端口自动避让,被占用时临时使用其他端口
|
|
||||||
- 新增系统代理和 TUN 权限引导,可直接安装服务并恢复原设置
|
|
||||||
- 新增服务模式多用户隔离,支持旧版服务升级迁移
|
|
||||||
- 新增手动取消节点固定功能
|
|
||||||
- 新增代理组标题栏拖动调整,可交换组名和工具栏的位置
|
|
||||||
- 新增前置规则与后置规则互相移动功能
|
|
||||||
- 新增 DNS 覆写保护:订阅含自定义 DNS 时先确认,订阅更新后自动关闭覆写
|
|
||||||
- 新增代理页排查提示,方便检查订阅和内核状态
|
|
||||||
- 新增订阅更新频率提醒,间隔不足 24 小时时提示
|
|
||||||
- 新增启动失败提示,显示原因并保存启动日志
|
|
||||||
- 新增 TUN「Mips」堆栈选项,需配合 mihomo v1.19.31 及以上内核
|
|
||||||
|
|
||||||
**🍎 macOS**
|
|
||||||
|
|
||||||
- 新增 macOS Liquid Glass 风格图标
|
|
||||||
- 新增 macOS 多用户启动保护,避免多个实例冲突
|
|
||||||
|
|
||||||
**🐧 Linux**
|
|
||||||
|
|
||||||
- 新增 Linux 系统主题跟随,自动切换深浅色外观
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> 🚀 优化改进 </strong></summary>
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
- 优化 TUN 配置优先级:界面设置优先,其他选项仍可通过订阅、覆写和脚本调整
|
|
||||||
- 优化系统代理失败后的恢复流程,保持系统设置和界面状态一致
|
|
||||||
- 优化错误提示,支持复制完整原因,托盘和快捷键操作失败时也会提醒
|
|
||||||
- 优化代理列表刷新、排序和页面切换速度
|
|
||||||
- 优化首页节点操作,显示节点固定状态
|
|
||||||
- 优化外部链接检查,仅允许打开 HTTP/HTTPS 网页
|
|
||||||
- 优化更新说明排版,支持表格和提示框
|
|
||||||
- 优化搜索框聚焦、界面字体和拖动排序动画
|
|
||||||
- 优化后台连接和托盘刷新,降低资源占用
|
|
||||||
- 优化解锁检测,支持同时检测多个项目并逐项显示结果
|
|
||||||
- 优化日志记录,减少重复信息
|
|
||||||
- 移除便携模式,统一使用系统应用数据目录
|
|
||||||
- 优化繁体中文、俄语翻译及语言名称显示
|
|
||||||
|
|
||||||
**🖥️ Windows**
|
**🖥️ Windows**
|
||||||
|
|
||||||
- 优化 Windows 自动更新,跳过安装语言选择
|
- Windows 系统代理默认绕过 `*.local` 域名,访问局域网设备的 `.local` 主机名时不再经过代理
|
||||||
|
|
||||||
**🐧 Linux**
|
|
||||||
|
|
||||||
- 优化 Linux 系统代理设置,减少额外命令依赖,并明确提示缺少的命令
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Languages:
|
Languages:
|
||||||
<a href="./README.md">简体中文</a> ·
|
|
||||||
<a href="./docs/README_en.md">English</a> ·
|
<a href="./docs/README_en.md">English</a> ·
|
||||||
<a href="./docs/README_es.md">Español</a> ·
|
<a href="./docs/README_es.md">Español</a> ·
|
||||||
<a href="./docs/README_ru.md">Русский</a> ·
|
<a href="./docs/README_fa.md">فارسی</a> ·
|
||||||
<a href="./docs/README_ja.md">日本語</a> ·
|
<a href="./docs/README_ja.md">日本語</a> ·
|
||||||
<a href="./docs/README_ko.md">한국어</a> ·
|
<a href="./docs/README_ko.md">한국어</a> ·
|
||||||
<a href="./docs/README_fa.md">فارسی</a>
|
<a href="./docs/README_pt.md">Português</a> ·
|
||||||
|
<a href="./docs/README_ru.md">Русский</a> ·
|
||||||
|
<a href="./README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
@@ -31,6 +32,7 @@ A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a
|
|||||||
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
|
||||||
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
Go to the [Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
|
||||||
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 11+ (intel/apple).
|
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 11+ (intel/apple).
|
||||||
|
支持 Windows (x64/x86)、Linux (x64/arm64) 和 macOS 11+ (intel/apple)。
|
||||||
|
|
||||||
#### 我应当怎样选择发行版
|
#### 我应当怎样选择发行版
|
||||||
|
|
||||||
@@ -50,14 +52,14 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 11+ (intel/apple).
|
|||||||
|
|
||||||
### ✈️ [AI云边 -- 全新架构机场 ClaudeBorder](https://cruise.54678999.xyz/#/register?code=58q5UJZc)
|
### ✈️ [AI云边 -- 全新架构机场 ClaudeBorder](https://cruise.54678999.xyz/#/register?code=58q5UJZc)
|
||||||
|
|
||||||
🔥热销中使用本链接注册即送 3 天免费试用**,每日 **1GB 流量**:👉 [点此注册](https://cruise.54678999.xyz/#/register?code=58q5UJZc)
|
🔥热销中使用本链接注册即送 **3 天免费试用**,每日 **1GB 流量**:👉 [点此注册](https://cruise.54678999.xyz/#/register?code=58q5UJZc)
|
||||||
|
|
||||||
#### AI云边 -- 全新架构机场。
|
#### AI云边 -- 全新架构机场。
|
||||||
|
|
||||||
- 💻 多次**技术迭代后**全新亮相。
|
- 💻 多次**技术迭代后**全新亮相。
|
||||||
- 🗺 全**高速稳定**正价节点。
|
- 🗺 全**高速稳定**正价节点。
|
||||||
- 🌏 **海外团队**,不跑路
|
- 🌏 **海外团队**,不跑路
|
||||||
- 🚀 线路**冗余**设计,自动化运维**对抗各类封锁
|
- 🚀 线路**冗余**设计,**自动化运维**对抗各类封锁
|
||||||
- 👨🦲 团队架构师为**大厂**网络架构师
|
- 👨🦲 团队架构师为**大厂**网络架构师
|
||||||
- 💰 极致**稳定**,亲民价**价格**
|
- 💰 极致**稳定**,亲民价**价格**
|
||||||
- 🌐 全面支持**流媒体及各AI访问**
|
- 🌐 全面支持**流媒体及各AI访问**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rust-i18n = "4.2.2"
|
rust-i18n = "4.2.4"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: على وشك الخروج
|
title: على وشك الخروج
|
||||||
body: Clash Verge على وشك الخروج.
|
body: Clash Verge على وشك الخروج.
|
||||||
appHidden:
|
|
||||||
title: تم إخفاء التطبيق
|
|
||||||
body: Clash Verge يعمل في الخلفية.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
adminInstallPrompt: يتطلب تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||||
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
adminUninstallPrompt: يتطلب إلغاء تثبيت خدمة Clash Verge صلاحيات المسؤول.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'تعذّر بدء النواة: توجد عملية نواة قيد التشغيل بالفعل. أغلق التطبيق المعني أو أنهِ العملية المتبقية، ثم أعد المحاولة. إذا استمرت المشكلة، فأعد تشغيل الكمبيوتر.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'تعذر تحديد موقع التطبيق: {error}'
|
currentExeError: 'تعذر تحديد موقع التطبيق: {error}'
|
||||||
homeDirError: 'تعذر تحديد المجلد الرئيسي للمستخدم الحالي. سيتم إنهاء التطبيق: {error}'
|
homeDirError: 'تعذر تحديد المجلد الرئيسي للمستخدم الحالي. سيتم إنهاء التطبيق: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: عام
|
global: عام
|
||||||
profiles: الملفات التعريفية
|
profiles: الملفات التعريفية
|
||||||
proxies: وكلاء
|
proxies: وكلاء
|
||||||
|
testLatency: اختبار التأخير
|
||||||
|
testingLatency: جارٍ اختبار التأخير…
|
||||||
systemProxy: وكيل النظام
|
systemProxy: وكيل النظام
|
||||||
tunMode: وضع TUN
|
tunMode: وضع TUN
|
||||||
closeAllConnections: إغلاق كل الاتصالات
|
closeAllConnections: إغلاق كل الاتصالات
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ notifications:
|
|||||||
body: Auf {mode} umgeschaltet.
|
body: Auf {mode} umgeschaltet.
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: Systemproxy
|
title: Systemproxy
|
||||||
'on': System proxy has been enabled.
|
'on': Der Systemproxy wurde aktiviert.
|
||||||
'off': System proxy has been disabled.
|
'off': Der Systemproxy wurde deaktiviert.
|
||||||
systemProxyFailed:
|
systemProxyFailed:
|
||||||
title: Systemproxy
|
title: Systemproxy
|
||||||
body: Der Systemproxy konnte nicht gesetzt werden. Öffnen Sie Clash Verge, um zu sehen, warum.
|
body: Der Systemproxy konnte nicht gesetzt werden. Öffnen Sie Clash Verge, um zu sehen, warum.
|
||||||
@@ -18,8 +18,8 @@ notifications:
|
|||||||
body: Nach mehreren Fehlversuchen in Folge gestoppt. Öffnen Sie Clash Verge, um zu sehen, warum.
|
body: Nach mehreren Fehlversuchen in Folge gestoppt. Öffnen Sie Clash Verge, um zu sehen, warum.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN-Modus
|
title: TUN-Modus
|
||||||
'on': TUN mode has been enabled.
|
'on': Der TUN-Modus wurde aktiviert.
|
||||||
'off': TUN mode has been disabled.
|
'off': Der TUN-Modus wurde deaktiviert.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Leichtmodus
|
title: Leichtmodus
|
||||||
body: Leichtmodus aktiviert.
|
body: Leichtmodus aktiviert.
|
||||||
@@ -29,17 +29,16 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: Beenden steht bevor
|
title: Beenden steht bevor
|
||||||
body: Clash Verge wird gleich beendet.
|
body: Clash Verge wird gleich beendet.
|
||||||
appHidden:
|
|
||||||
title: Anwendung ausgeblendet
|
|
||||||
body: Clash Verge läuft im Hintergrund.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash-Verge-Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: Eine neue Version (v{version}) wurde heruntergeladen und kann installiert werden.
|
||||||
installNow: Install Now
|
installNow: Jetzt installieren
|
||||||
later: Later
|
later: Später
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
adminInstallPrompt: Für die Installation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||||
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
adminUninstallPrompt: Für die Deinstallation des Clash-Verge-Dienstes sind Administratorrechte erforderlich.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Der Kern kann nicht gestartet werden: Ein Kernprozess läuft bereits. Beenden Sie die zugehörige App oder den verbliebenen Prozess und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, starten Sie den Computer neu.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'Der Speicherort der Anwendung konnte nicht ermittelt werden: {error}'
|
currentExeError: 'Der Speicherort der Anwendung konnte nicht ermittelt werden: {error}'
|
||||||
homeDirError: 'Das Benutzerverzeichnis des aktuellen Benutzers konnte nicht ermittelt werden. Die Anwendung wird beendet: {error}'
|
homeDirError: 'Das Benutzerverzeichnis des aktuellen Benutzers konnte nicht ermittelt werden. Die Anwendung wird beendet: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Profile
|
profiles: Profile
|
||||||
proxies: Proxy
|
proxies: Proxy
|
||||||
|
testLatency: Latenz testen
|
||||||
|
testingLatency: Latenz wird getestet…
|
||||||
systemProxy: Systemproxy
|
systemProxy: Systemproxy
|
||||||
tunMode: TUN-Modus
|
tunMode: TUN-Modus
|
||||||
closeAllConnections: Alle Verbindungen schließen
|
closeAllConnections: Alle Verbindungen schließen
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: About to Exit
|
title: About to Exit
|
||||||
body: Clash Verge is about to exit.
|
body: Clash Verge is about to exit.
|
||||||
appHidden:
|
|
||||||
title: Application Hidden
|
|
||||||
body: Clash Verge is running in the background.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
adminInstallPrompt: Installing the Clash Verge service requires administrator privileges.
|
||||||
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
adminUninstallPrompt: Uninstalling the Clash Verge service requires administrator privileges.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Cannot start the core: a core process is already running. Close the related app or stop the leftover process, then retry. If the problem persists, restart your computer.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'Unable to determine the application location: {error}'
|
currentExeError: 'Unable to determine the application location: {error}'
|
||||||
homeDirError: 'Unable to determine the current user''s home directory. The application will exit: {error}'
|
homeDirError: 'Unable to determine the current user''s home directory. The application will exit: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Profiles
|
profiles: Profiles
|
||||||
proxies: Proxies
|
proxies: Proxies
|
||||||
|
testLatency: Test Latency
|
||||||
|
testingLatency: Testing latency…
|
||||||
systemProxy: System Proxy
|
systemProxy: System Proxy
|
||||||
tunMode: TUN Mode
|
tunMode: TUN Mode
|
||||||
closeAllConnections: Close All Connections
|
closeAllConnections: Close All Connections
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: A punto de salir
|
title: A punto de salir
|
||||||
body: Clash Verge está a punto de salir.
|
body: Clash Verge está a punto de salir.
|
||||||
appHidden:
|
|
||||||
title: Aplicación oculta
|
|
||||||
body: Clash Verge se está ejecutando en segundo plano.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
|
adminInstallPrompt: Instalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||||
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
|
adminUninstallPrompt: Desinstalar el servicio de Clash Verge requiere privilegios de administrador.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'No se puede iniciar el núcleo: ya hay un proceso del núcleo en ejecución. Cierre la aplicación correspondiente o finalice el proceso restante y vuelva a intentarlo. Si el problema persiste, reinicie el equipo.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'No se pudo determinar la ubicación de la aplicación: {error}'
|
currentExeError: 'No se pudo determinar la ubicación de la aplicación: {error}'
|
||||||
homeDirError: 'No se pudo determinar la carpeta de inicio del usuario actual. La aplicación se cerrará: {error}'
|
homeDirError: 'No se pudo determinar la carpeta de inicio del usuario actual. La aplicación se cerrará: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Perfiles
|
profiles: Perfiles
|
||||||
proxies: Proxies
|
proxies: Proxies
|
||||||
|
testLatency: Probar latencia
|
||||||
|
testingLatency: Probando latencia…
|
||||||
systemProxy: Proxy del sistema
|
systemProxy: Proxy del sistema
|
||||||
tunMode: Modo TUN
|
tunMode: Modo TUN
|
||||||
closeAllConnections: Cerrar todas las conexiones
|
closeAllConnections: Cerrar todas las conexiones
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: در آستانه خروج
|
title: در آستانه خروج
|
||||||
body: Clash Verge در آستانه خروج است.
|
body: Clash Verge در آستانه خروج است.
|
||||||
appHidden:
|
|
||||||
title: برنامه پنهان شد
|
|
||||||
body: Clash Verge در پسزمینه در حال اجراست.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
adminInstallPrompt: نصب سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||||
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
adminUninstallPrompt: حذف سرویس Clash Verge به دسترسی مدیر نیاز دارد.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'هسته شروع نمیشود: یک فرایند هسته از قبل در حال اجراست. برنامه مرتبط را ببندید یا فرایند باقیمانده را متوقف کنید و دوباره تلاش کنید. اگر مشکل ادامه داشت، رایانه را راهاندازی مجدد کنید.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'مکان برنامه مشخص نشد: {error}'
|
currentExeError: 'مکان برنامه مشخص نشد: {error}'
|
||||||
homeDirError: 'پوشهٔ خانگی کاربر فعلی مشخص نشد. برنامه بسته خواهد شد: {error}'
|
homeDirError: 'پوشهٔ خانگی کاربر فعلی مشخص نشد. برنامه بسته خواهد شد: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: سراسری
|
global: سراسری
|
||||||
profiles: پروفایلها
|
profiles: پروفایلها
|
||||||
proxies: پروکسیها
|
proxies: پروکسیها
|
||||||
|
testLatency: آزمایش تأخیر
|
||||||
|
testingLatency: در حال آزمایش تأخیر…
|
||||||
systemProxy: پروکسی سیستم
|
systemProxy: پروکسی سیستم
|
||||||
tunMode: حالت TUN
|
tunMode: حالت TUN
|
||||||
closeAllConnections: بستن همه اتصالها
|
closeAllConnections: بستن همه اتصالها
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: Akan Keluar
|
title: Akan Keluar
|
||||||
body: Clash Verge akan keluar.
|
body: Clash Verge akan keluar.
|
||||||
appHidden:
|
|
||||||
title: Aplikasi Disembunyikan
|
|
||||||
body: Clash Verge berjalan di latar belakang.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
|
adminInstallPrompt: Menginstal layanan Clash Verge memerlukan hak administrator.
|
||||||
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
|
adminUninstallPrompt: Menghapus instalasi layanan Clash Verge memerlukan hak administrator.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Tidak dapat memulai inti: proses inti sudah berjalan. Tutup aplikasi terkait atau hentikan proses yang tersisa, lalu coba lagi. Jika masalah berlanjut, mulai ulang komputer.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'Lokasi aplikasi tidak dapat ditentukan: {error}'
|
currentExeError: 'Lokasi aplikasi tidak dapat ditentukan: {error}'
|
||||||
homeDirError: 'Direktori beranda pengguna saat ini tidak dapat ditentukan. Aplikasi akan ditutup: {error}'
|
homeDirError: 'Direktori beranda pengguna saat ini tidak dapat ditentukan. Aplikasi akan ditutup: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Global
|
global: Global
|
||||||
profiles: Profil
|
profiles: Profil
|
||||||
proxies: Proksi
|
proxies: Proksi
|
||||||
|
testLatency: Uji Latensi
|
||||||
|
testingLatency: Menguji latensi…
|
||||||
systemProxy: Proksi Sistem
|
systemProxy: Proksi Sistem
|
||||||
tunMode: Mode TUN
|
tunMode: Mode TUN
|
||||||
closeAllConnections: Tutup Semua Koneksi
|
closeAllConnections: Tutup Semua Koneksi
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: 終了間近
|
title: 終了間近
|
||||||
body: Clash Verge はまもなく終了します。
|
body: Clash Verge はまもなく終了します。
|
||||||
appHidden:
|
|
||||||
title: アプリが非表示
|
|
||||||
body: Clash Verge はバックグラウンドで実行中です。
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
|
adminInstallPrompt: Clash Verge サービスのインストールには管理者権限が必要です。
|
||||||
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
|
adminUninstallPrompt: Clash Verge サービスのアンインストールには管理者権限が必要です。
|
||||||
|
core:
|
||||||
|
alreadyRunning: コアを起動できません:コアプロセスが実行中です。関連アプリまたは残存プロセスを終了して再試行してください。解決しない場合は、コンピューターを再起動してください。
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: アプリケーションの場所を確認できませんでした:{error}
|
currentExeError: アプリケーションの場所を確認できませんでした:{error}
|
||||||
homeDirError: 現在のユーザーのホームディレクトリを確認できないため、アプリケーションを終了します:{error}
|
homeDirError: 現在のユーザーのホームディレクトリを確認できないため、アプリケーションを終了します:{error}
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: グローバル
|
global: グローバル
|
||||||
profiles: プロファイル
|
profiles: プロファイル
|
||||||
proxies: プロキシ
|
proxies: プロキシ
|
||||||
|
testLatency: 遅延テスト
|
||||||
|
testingLatency: 遅延をテスト中…
|
||||||
systemProxy: システムプロキシ
|
systemProxy: システムプロキシ
|
||||||
tunMode: TUN モード
|
tunMode: TUN モード
|
||||||
closeAllConnections: すべての接続を閉じる
|
closeAllConnections: すべての接続を閉じる
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: 곧 종료
|
title: 곧 종료
|
||||||
body: Clash Verge가 곧 종료됩니다.
|
body: Clash Verge가 곧 종료됩니다.
|
||||||
appHidden:
|
|
||||||
title: 앱이 숨겨짐
|
|
||||||
body: Clash Verge가 백그라운드에서 실행 중입니다.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
|
adminInstallPrompt: Clash Verge 서비스 설치에는 관리자 권한이 필요합니다.
|
||||||
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
|
adminUninstallPrompt: Clash Verge 서비스 제거에는 관리자 권한이 필요합니다.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 코어를 시작할 수 없습니다. 이미 실행 중인 코어 프로세스가 있습니다. 관련 앱이나 남아 있는 프로세스를 종료한 뒤 다시 시도하세요. 문제가 계속되면 컴퓨터를 다시 시작하세요.
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: '애플리케이션 위치를 확인할 수 없습니다: {error}'
|
currentExeError: '애플리케이션 위치를 확인할 수 없습니다: {error}'
|
||||||
homeDirError: '현재 사용자의 홈 디렉터리를 확인할 수 없어 애플리케이션을 종료합니다: {error}'
|
homeDirError: '현재 사용자의 홈 디렉터리를 확인할 수 없어 애플리케이션을 종료합니다: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: 글로벌
|
global: 글로벌
|
||||||
profiles: 프로필
|
profiles: 프로필
|
||||||
proxies: 프록시
|
proxies: 프록시
|
||||||
|
testLatency: 지연 테스트
|
||||||
|
testingLatency: 지연 테스트 중…
|
||||||
systemProxy: 시스템 프록시
|
systemProxy: 시스템 프록시
|
||||||
tunMode: TUN 모드
|
tunMode: TUN 모드
|
||||||
closeAllConnections: 모든 연결 닫기
|
closeAllConnections: 모든 연결 닫기
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
_version: 1
|
||||||
|
notifications:
|
||||||
|
dashboardToggled:
|
||||||
|
title: Painel
|
||||||
|
body: A visibilidade do painel foi atualizada.
|
||||||
|
clashModeChanged:
|
||||||
|
title: Troca de modo
|
||||||
|
body: Alterado para {mode}.
|
||||||
|
systemProxyToggled:
|
||||||
|
title: Proxy do sistema
|
||||||
|
'on': O proxy do sistema foi ativado.
|
||||||
|
'off': O proxy do sistema foi desativado.
|
||||||
|
systemProxyFailed:
|
||||||
|
title: Proxy do sistema
|
||||||
|
body: Não foi possível definir o proxy do sistema. Abra o Clash Verge para ver o motivo.
|
||||||
|
systemProxyGuardStopped:
|
||||||
|
title: Proteção do proxy do sistema
|
||||||
|
body: A proteção parou após várias falhas de execução. Abra o Clash Verge para ver o motivo.
|
||||||
|
tunModeToggled:
|
||||||
|
title: Modo TUN
|
||||||
|
'on': O modo TUN foi ativado.
|
||||||
|
'off': O modo TUN foi desativado.
|
||||||
|
lightweightModeEntered:
|
||||||
|
title: Modo leve
|
||||||
|
body: Modo leve ativado.
|
||||||
|
profilesReactivated:
|
||||||
|
title: Perfis
|
||||||
|
body: Perfil reativado.
|
||||||
|
appQuit:
|
||||||
|
title: Prestes a sair
|
||||||
|
body: O Clash Verge está prestes a sair.
|
||||||
|
updateReady:
|
||||||
|
title: Atualização do Clash Verge
|
||||||
|
body: Uma nova versão (v{version}) foi baixada e está pronta para instalar.
|
||||||
|
installNow: Instalar agora
|
||||||
|
later: Depois
|
||||||
|
service:
|
||||||
|
adminInstallPrompt: A instalação do serviço do Clash Verge requer privilégios de administrador.
|
||||||
|
adminUninstallPrompt: A desinstalação do serviço do Clash Verge requer privilégios de administrador.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Não é possível iniciar o núcleo: já há um processo do núcleo em execução. Feche o aplicativo correspondente ou encerre o processo remanescente e tente novamente. Se o problema persistir, reinicie o computador.'
|
||||||
|
launchGuard:
|
||||||
|
currentExeError: 'Não foi possível determinar a localização do aplicativo: {error}'
|
||||||
|
homeDirError: 'Não foi possível determinar o diretório pessoal do usuário atual. O aplicativo será encerrado: {error}'
|
||||||
|
translocated: O macOS está executando este aplicativo a partir de um caminho temporário de App Translocation. No Finder, mova-o manualmente para /Applications ou ~/Applications e abra-o novamente.
|
||||||
|
rejected: |-
|
||||||
|
Este aplicativo deve ser iniciado a partir de um diretório de Aplicativos.
|
||||||
|
|
||||||
|
{reason}
|
||||||
|
notInBundle: O executável não está dentro de um pacote de aplicativo.
|
||||||
|
canonicalizeBundleError: 'Não foi possível resolver o caminho do pacote do aplicativo: {error}'
|
||||||
|
bundleNameError: Não foi possível determinar o nome do pacote do aplicativo. O aplicativo será encerrado.
|
||||||
|
replacePrompt: Já existe um aplicativo em {destination}. Substituí-lo e reabrir a partir de Aplicativos?
|
||||||
|
movePrompt: Para iniciar o serviço em segundo plano com segurança, o aplicativo precisa ser movido para {destination}. Mover e reabrir agora?
|
||||||
|
createApplicationsDirError: 'Não foi possível criar o diretório de Aplicativos: {error}'
|
||||||
|
moveFailed: Falha ao mover o aplicativo. Mova-o manualmente no Finder e tente novamente.
|
||||||
|
replaceFailed: 'Não foi possível substituir o aplicativo existente com segurança: {error}'
|
||||||
|
relaunchFailed: O aplicativo foi copiado, mas não pôde ser reaberto automaticamente. Abra-o manualmente a partir de Aplicativos.
|
||||||
|
cancel: Cancelar
|
||||||
|
move: Mover
|
||||||
|
ok: OK
|
||||||
|
tray:
|
||||||
|
dashboard: Painel
|
||||||
|
ruleMode: Modo regra
|
||||||
|
globalMode: Modo global
|
||||||
|
directMode: Modo direto
|
||||||
|
outboundModes: Modos de saída
|
||||||
|
rule: Regra
|
||||||
|
direct: Direto
|
||||||
|
global: Global
|
||||||
|
profiles: Perfis
|
||||||
|
proxies: Proxies
|
||||||
|
testLatency: Test Latency
|
||||||
|
testingLatency: Testing latency…
|
||||||
|
systemProxy: Proxy do sistema
|
||||||
|
tunMode: Modo TUN
|
||||||
|
closeAllConnections: Fechar todas as conexões
|
||||||
|
lightweightMode: Modo leve
|
||||||
|
copyEnv: Copiar variáveis de ambiente
|
||||||
|
confDir: Diretório de configuração
|
||||||
|
coreDir: Diretório do núcleo
|
||||||
|
logsDir: Diretório de logs
|
||||||
|
openDir: Abrir diretório
|
||||||
|
appLog: Log do aplicativo
|
||||||
|
coreLog: Log do núcleo
|
||||||
|
restartClash: Reiniciar o núcleo do Clash
|
||||||
|
restartApp: Reiniciar o aplicativo
|
||||||
|
vergeVersion: Versão do Verge
|
||||||
|
more: Mais
|
||||||
|
exit: Sair
|
||||||
|
tooltip:
|
||||||
|
systemProxy: Proxy do sistema
|
||||||
|
tun: TUN
|
||||||
|
profile: Perfil
|
||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: Скорый выход
|
title: Скорый выход
|
||||||
body: Clash Verge скоро завершит работу.
|
body: Clash Verge скоро завершит работу.
|
||||||
appHidden:
|
|
||||||
title: Приложение скрыто
|
|
||||||
body: Clash Verge работает в фоновом режиме.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Обновление Clash Verge
|
title: Обновление Clash Verge
|
||||||
body: Новая версия (v{version}) скачана и готова к установке.
|
body: Новая версия (v{version}) скачана и готова к установке.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
|
adminInstallPrompt: Для установки службы Clash Verge требуются права администратора.
|
||||||
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
|
adminUninstallPrompt: Для удаления службы Clash Verge требуются права администратора.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Не удалось запустить ядро: процесс ядра уже работает. Закройте соответствующее приложение или завершите оставшийся процесс и повторите попытку. Если проблема сохраняется, перезагрузите компьютер.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'Не удалось определить расположение приложения: {error}'
|
currentExeError: 'Не удалось определить расположение приложения: {error}'
|
||||||
homeDirError: 'Не удалось определить домашний каталог текущего пользователя. Приложение будет закрыто: {error}'
|
homeDirError: 'Не удалось определить домашний каталог текущего пользователя. Приложение будет закрыто: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Глобальный
|
global: Глобальный
|
||||||
profiles: Профили
|
profiles: Профили
|
||||||
proxies: Прокси
|
proxies: Прокси
|
||||||
|
testLatency: Проверить задержку
|
||||||
|
testingLatency: Проверка задержки…
|
||||||
systemProxy: Системный прокси
|
systemProxy: Системный прокси
|
||||||
tunMode: Режим TUN
|
tunMode: Режим TUN
|
||||||
closeAllConnections: Закрыть все соединения
|
closeAllConnections: Закрыть все соединения
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ notifications:
|
|||||||
body: '{mode} moduna geçildi.'
|
body: '{mode} moduna geçildi.'
|
||||||
systemProxyToggled:
|
systemProxyToggled:
|
||||||
title: Sistem Vekil'i
|
title: Sistem Vekil'i
|
||||||
'on': System proxy has been enabled.
|
'on': Sistem vekili etkinleştirildi.
|
||||||
'off': System proxy has been disabled.
|
'off': Sistem vekili devre dışı bırakıldı.
|
||||||
systemProxyFailed:
|
systemProxyFailed:
|
||||||
title: Sistem Vekili
|
title: Sistem Vekili
|
||||||
body: Sistem vekil sunucusu ayarlanamadı. Nedenini görmek için Clash Verge'i açın.
|
body: Sistem vekil sunucusu ayarlanamadı. Nedenini görmek için Clash Verge'i açın.
|
||||||
@@ -18,8 +18,8 @@ notifications:
|
|||||||
body: Art arda birkaç başarısızlıktan sonra durdu. Nedenini görmek için Clash Verge'i açın.
|
body: Art arda birkaç başarısızlıktan sonra durdu. Nedenini görmek için Clash Verge'i açın.
|
||||||
tunModeToggled:
|
tunModeToggled:
|
||||||
title: TUN Modu
|
title: TUN Modu
|
||||||
'on': TUN mode has been enabled.
|
'on': TUN modu etkinleştirildi.
|
||||||
'off': TUN mode has been disabled.
|
'off': TUN modu devre dışı bırakıldı.
|
||||||
lightweightModeEntered:
|
lightweightModeEntered:
|
||||||
title: Hafif Mod
|
title: Hafif Mod
|
||||||
body: Hafif moda geçildi.
|
body: Hafif moda geçildi.
|
||||||
@@ -29,17 +29,16 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: Çıkış Yapılmak Üzere
|
title: Çıkış Yapılmak Üzere
|
||||||
body: Clash Verge kapanmak üzere.
|
body: Clash Verge kapanmak üzere.
|
||||||
appHidden:
|
|
||||||
title: Uygulama Gizlendi
|
|
||||||
body: Clash Verge arka planda çalışıyor.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Güncellemesi
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: Yeni bir sürüm (v{version}) indirildi ve kurulmaya hazır.
|
||||||
installNow: Install Now
|
installNow: Şimdi Kur
|
||||||
later: Later
|
later: Daha Sonra
|
||||||
service:
|
service:
|
||||||
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
|
adminInstallPrompt: Clash Verge hizmetini kurmak için yönetici ayrıcalıkları gerekir.
|
||||||
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
|
adminUninstallPrompt: Clash Verge hizmetini kaldırmak için yönetici ayrıcalıkları gerekir.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Çekirdek başlatılamıyor: bir çekirdek işlemi zaten çalışıyor. İlgili uygulamayı kapatın veya kalan işlemi sonlandırıp yeniden deneyin. Sorun devam ederse bilgisayarı yeniden başlatın.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'Uygulamanın konumu belirlenemedi: {error}'
|
currentExeError: 'Uygulamanın konumu belirlenemedi: {error}'
|
||||||
homeDirError: 'Geçerli kullanıcının ana dizini belirlenemedi. Uygulama kapatılacak: {error}'
|
homeDirError: 'Geçerli kullanıcının ana dizini belirlenemedi. Uygulama kapatılacak: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Küresel
|
global: Küresel
|
||||||
profiles: Profiller
|
profiles: Profiller
|
||||||
proxies: Vekil'ler
|
proxies: Vekil'ler
|
||||||
|
testLatency: Gecikmeyi Test Et
|
||||||
|
testingLatency: Gecikme test ediliyor…
|
||||||
systemProxy: Sistem Vekil'i
|
systemProxy: Sistem Vekil'i
|
||||||
tunMode: TUN Modu
|
tunMode: TUN Modu
|
||||||
closeAllConnections: Tüm Bağlantıları Kapat
|
closeAllConnections: Tüm Bağlantıları Kapat
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: Чыгар алдыннан
|
title: Чыгар алдыннан
|
||||||
body: Clash Verge чыгарга җыена.
|
body: Clash Verge чыгарга җыена.
|
||||||
appHidden:
|
|
||||||
title: Кушымта яшерелде
|
|
||||||
body: Clash Verge фон режимында эшли.
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge Update
|
title: Clash Verge Update
|
||||||
body: A new version (v{version}) has been downloaded and is ready to install.
|
body: A new version (v{version}) has been downloaded and is ready to install.
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
|
adminInstallPrompt: Clash Verge хезмәтен урнаштыру өчен администратор хокуклары кирәк.
|
||||||
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
|
adminUninstallPrompt: Clash Verge хезмәтен бетерү өчен администратор хокуклары кирәк.
|
||||||
|
core:
|
||||||
|
alreadyRunning: 'Төшне башлап булмый: төш процессы инде эшли. Тиешле кушымтаны ябыгыз яки калган процессны тәмамлагыз һәм яңадан сынап карагыз. Проблема хәл ителмәсә, санакны яңадан кабызыгыз.'
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 'Кушымтаның урнашу урынын билгеләп булмады: {error}'
|
currentExeError: 'Кушымтаның урнашу урынын билгеләп булмады: {error}'
|
||||||
homeDirError: 'Хәзерге кулланучының өй каталогын билгеләп булмады. Кушымта ябылачак: {error}'
|
homeDirError: 'Хәзерге кулланучының өй каталогын билгеләп булмады. Кушымта ябылачак: {error}'
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: Глобаль
|
global: Глобаль
|
||||||
profiles: Профильләр
|
profiles: Профильләр
|
||||||
proxies: Проксилар
|
proxies: Проксилар
|
||||||
|
testLatency: Кичектерүне тикшерү
|
||||||
|
testingLatency: Кичектерү тикшерелә…
|
||||||
systemProxy: Системалы прокси
|
systemProxy: Системалы прокси
|
||||||
tunMode: TUN режимы
|
tunMode: TUN режимы
|
||||||
closeAllConnections: Барлык тоташуларны ябу
|
closeAllConnections: Барлык тоташуларны ябу
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: 即将退出
|
title: 即将退出
|
||||||
body: Clash Verge 即将退出。
|
body: Clash Verge 即将退出。
|
||||||
appHidden:
|
|
||||||
title: 应用已隐藏
|
|
||||||
body: Clash Verge 正在后台运行。
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge 更新
|
title: Clash Verge 更新
|
||||||
body: 新版本 (v{version}) 已下载完成,是否立即安装?
|
body: 新版本 (v{version}) 已下载完成,是否立即安装?
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: 安装 Clash Verge 服务需要管理员权限
|
adminInstallPrompt: 安装 Clash Verge 服务需要管理员权限
|
||||||
adminUninstallPrompt: 卸载 Clash Verge 服务需要管理员权限
|
adminUninstallPrompt: 卸载 Clash Verge 服务需要管理员权限
|
||||||
|
core:
|
||||||
|
alreadyRunning: 无法启动内核:已有内核进程正在运行。请退出相关应用或清理残留进程后重试。若仍无法解决,请重启电脑。
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 无法确定应用位置:{error}
|
currentExeError: 无法确定应用位置:{error}
|
||||||
homeDirError: 无法确定当前用户主目录,应用将退出:{error}
|
homeDirError: 无法确定当前用户主目录,应用将退出:{error}
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: 全局
|
global: 全局
|
||||||
profiles: 订阅
|
profiles: 订阅
|
||||||
proxies: 代理
|
proxies: 代理
|
||||||
|
testLatency: 测试延迟
|
||||||
|
testingLatency: 正在测试延迟…
|
||||||
systemProxy: 系统代理
|
systemProxy: 系统代理
|
||||||
tunMode: TUN 模式
|
tunMode: TUN 模式
|
||||||
closeAllConnections: 关闭所有连接
|
closeAllConnections: 关闭所有连接
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ notifications:
|
|||||||
appQuit:
|
appQuit:
|
||||||
title: 即將退出
|
title: 即將退出
|
||||||
body: Clash Verge 即將退出。
|
body: Clash Verge 即將退出。
|
||||||
appHidden:
|
|
||||||
title: 應用已隱藏
|
|
||||||
body: Clash Verge 正在背景執行。
|
|
||||||
updateReady:
|
updateReady:
|
||||||
title: Clash Verge 更新
|
title: Clash Verge 更新
|
||||||
body: 新版本(v{version})已下載完成,是否立即安裝?
|
body: 新版本(v{version})已下載完成,是否立即安裝?
|
||||||
@@ -40,6 +37,8 @@ notifications:
|
|||||||
service:
|
service:
|
||||||
adminInstallPrompt: 安裝 Clash Verge 服務需要管理員權限
|
adminInstallPrompt: 安裝 Clash Verge 服務需要管理員權限
|
||||||
adminUninstallPrompt: 解除安裝 Clash Verge 服務需要管理員權限
|
adminUninstallPrompt: 解除安裝 Clash Verge 服務需要管理員權限
|
||||||
|
core:
|
||||||
|
alreadyRunning: 無法啟動核心:已有核心程序正在執行。請結束相關應用程式或清理殘留程序後重試。若仍無法解決,請重新啟動電腦。
|
||||||
launchGuard:
|
launchGuard:
|
||||||
currentExeError: 無法判斷應用程式位置:{error}
|
currentExeError: 無法判斷應用程式位置:{error}
|
||||||
homeDirError: 無法判斷目前使用者的個人目錄,應用程式將結束:{error}
|
homeDirError: 無法判斷目前使用者的個人目錄,應用程式將結束:{error}
|
||||||
@@ -71,6 +70,8 @@ tray:
|
|||||||
global: 全域
|
global: 全域
|
||||||
profiles: 訂閱
|
profiles: 訂閱
|
||||||
proxies: 代理
|
proxies: 代理
|
||||||
|
testLatency: 測試延遲
|
||||||
|
testingLatency: 正在測試延遲…
|
||||||
systemProxy: 系統代理
|
systemProxy: 系統代理
|
||||||
tunMode: 虛擬網路介面卡模式
|
tunMode: 虛擬網路介面卡模式
|
||||||
closeAllConnections: 關閉所有連線
|
closeAllConnections: 關閉所有連線
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
|
||||||
clash-verge-logging = { workspace = true }
|
clash-verge-logging = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
|
||||||
pub struct UnlockItem {
|
pub struct UnlockItem {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ edition = "2024"
|
|||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
specta = { version = "=2.0.0-rc.26", features = ["derive", "function"] }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
tauri-plugin-clipboard-manager = { workspace = true }
|
tauri-plugin-clipboard-manager = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
@@ -14,7 +15,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
sysinfo = { version = "0.39.6", features = ["network", "system"] }
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies]
|
[target.'cfg(not(windows))'.dependencies]
|
||||||
libc = "0.2.189"
|
libc = "0.2.190"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
deelevate = { workspace = true }
|
deelevate = { workspace = true }
|
||||||
|
|||||||
@@ -1,53 +1,46 @@
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::{AppHandle, Runtime, State, command};
|
use tauri::{AppHandle, Runtime, State, command};
|
||||||
use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
|
use tauri_plugin_clipboard_manager::ClipboardExt as _;
|
||||||
|
|
||||||
use crate::Platform;
|
use crate::Platform;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, specta::Type)]
|
||||||
pub struct SystemInfo {
|
pub struct SystemInfo {
|
||||||
pub system_name: String,
|
pub system_name: String,
|
||||||
pub system_version: String,
|
pub system_version: String,
|
||||||
pub system_kernel_version: String,
|
|
||||||
pub system_arch: String,
|
|
||||||
pub app_version: String,
|
|
||||||
pub app_core_mode: String,
|
|
||||||
pub app_is_admin: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Platform> for SystemInfo {
|
impl From<&Platform> for SystemInfo {
|
||||||
fn from(platform: Platform) -> Self {
|
fn from(platform: &Platform) -> Self {
|
||||||
Self {
|
Self {
|
||||||
system_name: platform.sysinfo.system_name,
|
system_name: platform.sysinfo.system_name.clone(),
|
||||||
system_version: platform.sysinfo.system_version,
|
system_version: platform.sysinfo.system_version.clone(),
|
||||||
system_kernel_version: platform.sysinfo.system_kernel_version,
|
|
||||||
system_arch: platform.sysinfo.system_arch,
|
|
||||||
app_version: platform.appinfo.app_version,
|
|
||||||
app_core_mode: platform.appinfo.app_core_mode,
|
|
||||||
app_is_admin: platform.appinfo.app_is_admin,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, Error> {
|
#[specta::specta]
|
||||||
|
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, String> {
|
||||||
let platform = state.inner().read();
|
let platform = state.inner().read();
|
||||||
Ok(SystemInfo::from(platform.clone()))
|
Ok(SystemInfo::from(&*platform))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取应用的运行时间(毫秒)
|
/// 获取应用的运行时间(毫秒)
|
||||||
#[command]
|
#[command]
|
||||||
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
|
#[specta::specta]
|
||||||
|
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, String> {
|
||||||
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
|
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn export_diagnostic_info<R: Runtime>(
|
pub fn export_diagnostic_info<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
state: State<'_, RwLock<Platform>>,
|
state: State<'_, RwLock<Platform>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), String> {
|
||||||
let info = state.inner().read().to_string();
|
let info = state.inner().read().to_string();
|
||||||
let clipboard = app_handle.clipboard();
|
let clipboard = app_handle.clipboard();
|
||||||
clipboard.write_text(info)
|
clipboard.write_text(info).map_err(|error| error.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,269 @@
|
|||||||
|
## v2.5.8
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复系统托盘代理组行不显示延迟、超时项与未测过项无法区分的问题
|
||||||
|
- 修复不同订阅源的同名节点测速和延迟显示指向错误节点的问题
|
||||||
|
- 修复端口设置混淆保存值与运行值、输入无效时缺少提示的问题
|
||||||
|
- 修复已有内核进程阻止启动时缺少多语言说明和处理建议的问题
|
||||||
|
- 修复从开始菜单再次打开应用后,窗口反复弹出并提示启动失败的问题
|
||||||
|
- 修复多个代理集合包含同名节点时,代理组中的节点变灰并显示 ambiguous、无法选择的问题
|
||||||
|
- 修复手动更新时重复下载后台已下载的安装包、启动时选「稍后」后每次启动都重新下载的问题
|
||||||
|
- 修复下载更新时镜像缓慢或卡住不会切换下载地址、点击「取消」无法中止下载的问题
|
||||||
|
- 修复关闭「自动检查更新」后,启动时仍弹出已下载更新的安装提示的问题
|
||||||
|
- 修复从 Dock 退出、注销或关机时,应用可能长时间卡住无法退出的问题
|
||||||
|
- 修复宽窗口下「订阅」页每行显示过多卡片,导致配置名称被截断的问题
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 修复 Windows 升级后因文件权限异常无法启动、需要手动清理配置的问题
|
||||||
|
- 修复 Windows 服务启动类型被改为手动后,软件启动卡住约两分钟的问题,并提供一键修复
|
||||||
|
- 修复 Windows 长时间运行后,托盘菜单点击无响应的问题
|
||||||
|
|
||||||
|
**🍎 macOS**
|
||||||
|
|
||||||
|
- 修复 macOS 关闭 TUN 或退出后,系统 DNS 仍停留在 `114.114.114.114` 导致无法解析域名的问题
|
||||||
|
|
||||||
|
**🖥️/🍎 Windows/macOS**
|
||||||
|
|
||||||
|
- 修复 Windows/macOS 打开系统托盘菜单时,节点延迟与自动选择状态未刷新的问题
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
|
- 新增系统托盘代理组中的「测试延迟」菜单项
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
|
- 优化端口设置窗口的实现,精简重复代码
|
||||||
|
- 优化侧边栏流量图表的 CPU 占用
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 优化 Windows 服务安全检查未通过时的提示:启动、安装或修复服务时说明原因与内核占用,并提供处理方法
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v2.5.7
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复读屏软件将普通模式下的侧边栏导航项播报为不可用的可拖拽控件的问题
|
||||||
|
- 修复订阅包含自定义 DNS 时,确认开启的 DNS 覆写在重启应用后被自动关闭的问题
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 修复 Windows 系统盘根目录的删除权限被误判,导致服务模式和 TUN 无法使用的问题
|
||||||
|
- 修复 Windows 部分系统安装服务失败,提示「program not found」的问题
|
||||||
|
|
||||||
|
**🍎 macOS**
|
||||||
|
|
||||||
|
- macOS 修复启动或重启应用时偶发内核启动失败,并误提示「需要更新系统服务」的问题
|
||||||
|
- macOS 修复 VPN 接管网络或开机网络未就绪时的问题:服务模式内核无法启动、TUN 不可用、系统代理状态读取报错
|
||||||
|
- macOS 修复用 `ipconfig set` 手动配置网卡后无法设置系统代理的问题
|
||||||
|
|
||||||
|
**🍎/🐧 macOS/Linux**
|
||||||
|
|
||||||
|
- 修复 macOS/Linux 服务模式停止或重启内核时直接强制结束的问题:内核来不及正常退出、最后的日志丢失
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
|
- 优化服务模式的问题排查:服务自身的运行日志会保存到文件,便于追查内核被停止的原因
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 优化 Windows 服务安全检查未通过时的启动提示:不再只显示「服务无法启动内核」,直接说明原因并提供修复文档链接
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## v2.5.6
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复「代理集合」「规则集合」订阅响应较慢时更新误报失败、且不显示真实失败原因的问题
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 修复 Windows 部分未安装服务的用户以普通权限运行时,内核无法启动的问题
|
||||||
|
- 修复 Windows 系统隔离权限被误判,导致服务模式和 TUN 无法使用的问题
|
||||||
|
- 修复 Windows 旧版服务状态残留导致无法重装的问题,自动备份可识别的残留并恢复安装
|
||||||
|
- 修复 Windows 服务已停止时,启动要等约两分钟才显示窗口的问题
|
||||||
|
- 优化 Windows 服务安全检查失败提示:用易懂的文字说明原因,并提供修复文档链接
|
||||||
|
|
||||||
|
**🍎 macOS**
|
||||||
|
|
||||||
|
- macOS 修复编辑规则时「规则内容」输入的首字母被自动转为大写的问题
|
||||||
|
- macOS 修复残留的服务进程导致内核无法启动、「继续使用 Sidecar」失败且无法修复的问题
|
||||||
|
- macOS 修复部分账户安装或修复服务时提示无法解析用户组而失败的问题
|
||||||
|
- macOS 优化用户目录在外置磁盘时服务拒绝启动内核的提示:说明原因并给出修复命令
|
||||||
|
- macOS 修复服务模式内核意外停止后,系统代理仍指向失效端口导致无法上网的问题
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
|
- 优化内核启动失败和服务模式内核意外停止的提示:显示具体原因,并在窗口恢复后提示尚未解决的错误
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v2.5.5
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复不同来源的订阅或规则集共用缓存路径时,服务模式无法启动内核的问题
|
||||||
|
- 优化文件错误日志,显示具体文件路径和失败原因
|
||||||
|
- 修复 DNS 覆写:空内容报错、初始值覆盖已有设置、已填写字段残留原配置、空字段在高级编辑器中缺失
|
||||||
|
- 修复 DNS 覆写中已填写的字段被 Merge 或 Script 覆盖的问题,空字段继续使用扩展配置
|
||||||
|
- 修复 Merge 覆写 DNS 策略、过滤条件和 hosts 时,旧条目仍被保留的问题
|
||||||
|
- 修复首次以管理员身份启动后,系统服务因数据目录所有权不符而拒绝启动内核的问题
|
||||||
|
- 修复重新安装服务后内核缺失、无法继续使用 Sidecar 的问题
|
||||||
|
- 修复内核未运行时,首页仍显示服务模式的问题
|
||||||
|
- 修复服务重装重复请求管理员授权、卸载后误报失败的问题
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 修复 Windows 服务因路径或目录权限问题无法启动时,缺少重新安装服务的提示
|
||||||
|
- 修复 Windows 服务回退检查失败后状态未更新、无法重新尝试 Sidecar 的问题
|
||||||
|
|
||||||
|
**🐧 Linux**
|
||||||
|
|
||||||
|
- Linux 修复安装或修复服务后仍无法启动内核、开启 TUN 模式的问题
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
|
- 优化扩展覆写配置和脚本编辑窗口,提示作用范围、执行顺序及应用设置优先级,并高亮当前步骤
|
||||||
|
- 优化 DNS 覆写开关:按订阅独立记忆,自动关闭仅影响当前订阅,并提示作用范围
|
||||||
|
- 优化系统服务兼容性检查:升级后服务版本或协议不兼容时,提示重新安装
|
||||||
|
- 优化预构建资源,补充 ASN 数据库文件
|
||||||
|
- 扩展配置或脚本写入由应用设置接管的字段时,弹出提示并在扩展日志中记录该值已被丢弃
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## v2.5.4
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复首页代理组和节点下拉菜单:再次点击无法收起或误选、小窗口显示不全、切换模式后意外展开
|
||||||
|
- 修复系统代理切换失败后,开关状态显示错误的问题
|
||||||
|
- 修复代理守卫失败后反复重试且没有提示的问题
|
||||||
|
- 修复未开启系统代理时,启动仍提示代理错误的问题
|
||||||
|
- 修复恢复备份后未按备份设置关闭系统代理的问题
|
||||||
|
- 修复服务模式下操作超时后,后续操作报错或状态异常的问题
|
||||||
|
- 修复部分节点显示「Unknown」或无法测试延迟的问题
|
||||||
|
- 修复 `PASS-RULE` 节点被错误隐藏的问题
|
||||||
|
- 修复重启内核后未恢复上次所选节点的问题
|
||||||
|
- 修复部分代理组和首页图标无法显示、页面报错的问题
|
||||||
|
- 修复同时修改端口、TUN 等设置时,部分修改丢失的问题
|
||||||
|
- 修复 TUN 模式下 IPv6 Fake-IP 默认地址范围错误的问题
|
||||||
|
- 修复切换节点时应用可能卡住的问题
|
||||||
|
- 修复订阅文件被误删、删除或排序后列表异常的问题
|
||||||
|
- 修复订阅自动更新失败无提示、重启后更新时间延后及状态误报的问题
|
||||||
|
- 修复订阅编辑器因节点名称为空而崩溃、因格式错误而丢失内容的问题
|
||||||
|
- 修复 DNS 编辑器空内容报错、初始值覆盖已有设置的问题
|
||||||
|
- 修复关闭自动检查更新后,手动检查更新无效的问题
|
||||||
|
- 修复应用冷启动或从轻量模式打开界面时, 系统代理被重复设置的问题
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 修复 Windows 管理员运行时,服务权限校验失败后无法继续使用代理的问题
|
||||||
|
- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题
|
||||||
|
- 修复 Windows 系统代理设置偶发失败的问题
|
||||||
|
- 修复 Windows 关机被应用阻止的问题
|
||||||
|
- 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加
|
||||||
|
- 修复 Windows 配置中 GEO 规则较多时,首次启动内核失败的问题
|
||||||
|
|
||||||
|
**🍎 macOS**
|
||||||
|
|
||||||
|
- 修复 macOS 内核更新失败后无法启动的问题
|
||||||
|
- 修复 macOS 需要管理员授权或开机自启时,系统代理设置失败的问题
|
||||||
|
- 修复 macOS 切换显示器后托盘速率样式不同步的问题
|
||||||
|
|
||||||
|
**🐧 Linux**
|
||||||
|
|
||||||
|
- 修复 Linux 安装或卸载服务时授权失败的问题
|
||||||
|
- 修复 Fedora 等启用 SELinux 的系统更新后服务无法启动的问题
|
||||||
|
|
||||||
|
**🍎/🐧 macOS/Linux**
|
||||||
|
|
||||||
|
- 修复 macOS/Linux 系统代理绕过规则未正确生效的问题
|
||||||
|
- 修复 macOS/Linux 因旧版服务缓存权限异常而无法记住节点的问题
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
|
- 新增混合代理端口自动避让,被占用时临时使用其他端口
|
||||||
|
- 新增系统代理和 TUN 权限引导,可直接安装服务并恢复原设置
|
||||||
|
- 新增服务模式多用户隔离,支持旧版服务升级迁移
|
||||||
|
- 新增手动取消节点固定功能
|
||||||
|
- 新增代理组标题栏拖动调整,可交换组名和工具栏的位置
|
||||||
|
- 新增前置规则与后置规则互相移动功能
|
||||||
|
- 新增 DNS 覆写保护:订阅含自定义 DNS 时先确认,订阅更新后自动关闭覆写
|
||||||
|
- 新增代理页排查提示,方便检查订阅和内核状态
|
||||||
|
- 新增订阅更新频率提醒,间隔不足 24 小时时提示
|
||||||
|
- 新增启动失败提示,显示原因并保存启动日志
|
||||||
|
- 新增 TUN「Mips」堆栈选项,需配合 mihomo v1.19.31 及以上内核
|
||||||
|
|
||||||
|
**🍎 macOS**
|
||||||
|
|
||||||
|
- 新增 macOS Liquid Glass 风格图标
|
||||||
|
- 新增 macOS 多用户启动保护,避免多个实例冲突
|
||||||
|
|
||||||
|
**🐧 Linux**
|
||||||
|
|
||||||
|
- 新增 Linux 系统主题跟随,自动切换深浅色外观
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong> 🚀 优化改进 </strong></summary>
|
||||||
|
|
||||||
|
- 优化首页节点菜单关闭时的刷新性能
|
||||||
|
- 优化 TUN 配置优先级:界面设置优先,其他选项仍可通过订阅、覆写和脚本调整
|
||||||
|
- 优化 DNS 覆写:仅覆盖已填写的字段和已开启的开关,留空或关闭的项保留订阅设置
|
||||||
|
- 优化系统代理失败后的恢复流程,保持系统设置和界面状态一致
|
||||||
|
- 优化错误提示,支持复制完整原因,托盘和快捷键操作失败时也会提醒
|
||||||
|
- 优化代理列表刷新、排序和页面切换速度
|
||||||
|
- 优化首页节点操作,显示节点固定状态
|
||||||
|
- 优化外部链接检查,仅允许打开 HTTP/HTTPS 网页
|
||||||
|
- 优化更新说明排版,支持表格和提示框
|
||||||
|
- 优化搜索框聚焦、界面字体和拖动排序动画
|
||||||
|
- 优化后台连接和托盘刷新,降低资源占用
|
||||||
|
- 优化解锁检测,支持同时检测多个项目并逐项显示结果
|
||||||
|
- 优化日志记录,减少重复信息
|
||||||
|
- 移除便携模式,统一使用系统应用数据目录
|
||||||
|
- 优化繁体中文、俄语翻译及语言名称显示
|
||||||
|
|
||||||
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 优化 Windows 自动更新,跳过安装语言选择
|
||||||
|
|
||||||
|
**🐧 Linux**
|
||||||
|
|
||||||
|
- 优化 Linux 系统代理设置,减少额外命令依赖,并明确提示缺少的命令
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
## v2.5.2
|
## v2.5.2
|
||||||
|
|
||||||
### 🐞 修复问题
|
### 🐞 修复问题
|
||||||
|
|||||||
+7
-6
@@ -11,13 +11,14 @@ A Clash Meta GUI built with <a href="https://github.com/tauri-apps/tauri">Tauri<
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Languages:
|
Languages:
|
||||||
<a href="../README.md">简体中文</a> ·
|
|
||||||
<a href="./README_en.md">English</a> ·
|
<a href="./README_en.md">English</a> ·
|
||||||
<a href="./README_es.md">Español</a> ·
|
<a href="./README_es.md">Español</a> ·
|
||||||
<a href="./README_ru.md">Русский</a> ·
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
<a href="./README_ja.md">日本語</a> ·
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
<a href="./README_ko.md">한국어</a> ·
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
<a href="./README_fa.md">فارسی</a>
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
@@ -51,11 +52,11 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
|
|||||||
|
|
||||||
## Promotion
|
## Promotion
|
||||||
|
|
||||||
### ✈️ [Doggygo VPN — A Technical-Grade Proxy Service](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
### ✈️ [Doggygo VPN — A Technical-Grade Proxy Service](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
🚀 A high-performance, overseas, technical-grade proxy service offering free trials and discounted plans, fully unlocking streaming platforms and AI services. The world’s first provider to adopt the **QUIC protocol**.
|
🚀 A high-performance, overseas, technical-grade proxy service offering free trials and discounted plans, fully unlocking streaming platforms and AI services. The world’s first provider to adopt the **QUIC protocol**.
|
||||||
|
|
||||||
🎁 Register via the **Clash Verge exclusive invitation link** to receive **3 days of free trial**, with **1GB traffic per day**: 👉 [Register here](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
🎁 Register via the **Clash Verge exclusive invitation link** to receive **3 days of free trial**, with **1GB traffic per day**: 👉 [Register here](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
#### **Core Advantages:**
|
#### **Core Advantages:**
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ Join [@clash_verge_rev](https://t.me/clash_verge_re) for update announcements.
|
|||||||
- ⚡ The world’s first **QUIC-protocol-based proxy service**, now featuring faster **QUIC-family protocols** (best paired with the Clash Verge client)
|
- ⚡ The world’s first **QUIC-protocol-based proxy service**, now featuring faster **QUIC-family protocols** (best paired with the Clash Verge client)
|
||||||
- 🎬 Unlocks **streaming platforms and mainstream AI services**
|
- 🎬 Unlocks **streaming platforms and mainstream AI services**
|
||||||
|
|
||||||
🌐 Official Website: 👉 [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
🌐 Official Website: 👉 [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
### 🤖 [GPTKefu — AI-Powered Customer Service Platform Deeply Integrated with Crisp](https://gptkefu.com)
|
### 🤖 [GPTKefu — AI-Powered Customer Service Platform Deeply Integrated with Crisp](https://gptkefu.com)
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -11,13 +11,14 @@ Una interfaz gráfica para Clash Meta construida con <a href="https://github.com
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Idiomas:
|
Idiomas:
|
||||||
<a href="../README.md">简体中文</a> ·
|
|
||||||
<a href="./README_en.md">English</a> ·
|
<a href="./README_en.md">English</a> ·
|
||||||
<a href="./README_es.md">Español</a> ·
|
<a href="./README_es.md">Español</a> ·
|
||||||
<a href="./README_ru.md">Русский</a> ·
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
<a href="./README_ja.md">日本語</a> ·
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
<a href="./README_ko.md">한국어</a> ·
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
<a href="./README_fa.md">فارسی</a>
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Vista previa
|
## Vista previa
|
||||||
@@ -51,17 +52,17 @@ Consulta la [documentación del proyecto](https://clash-verge-rev.github.io/) pa
|
|||||||
|
|
||||||
## Promociones
|
## Promociones
|
||||||
|
|
||||||
#### [Doggygo VPN — Acelerador global orientado al rendimiento](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
#### [Doggygo VPN — Acelerador global orientado al rendimiento](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
- Servicio internacional de alto rendimiento con prueba gratuita, planes con descuento, desbloqueo de streaming y soporte de protocolo Hysteria de primera clase.
|
- Servicio internacional de alto rendimiento con prueba gratuita, planes con descuento, desbloqueo de streaming y soporte de protocolo Hysteria de primera clase.
|
||||||
- Regístrate mediante el enlace exclusivo de Clash Verge y obtén una prueba de 3 días con 1 GB de tráfico diario: [Regístrate](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Regístrate mediante el enlace exclusivo de Clash Verge y obtén una prueba de 3 días con 1 GB de tráfico diario: [Regístrate](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
- Cupón exclusivo de 20% de descuento para usuarios de Clash Verge: `verge20` (limitado a 500 usos)
|
- Cupón exclusivo de 20% de descuento para usuarios de Clash Verge: `verge20` (limitado a 500 usos)
|
||||||
- Plan promocional desde ¥15.8 al mes con 160 GB, más 20% de descuento adicional por pago anual
|
- Plan promocional desde ¥15.8 al mes con 160 GB, más 20% de descuento adicional por pago anual
|
||||||
- Equipo ubicado en el extranjero para un servicio confiable, con hasta 50% de comisión compartida
|
- Equipo ubicado en el extranjero para un servicio confiable, con hasta 50% de comisión compartida
|
||||||
- Clústeres balanceados con rutas dedicadas de alta velocidad (compatibles con clientes antiguos), latencia extremadamente baja, reproducción 4K sin interrupciones
|
- Clústeres balanceados con rutas dedicadas de alta velocidad (compatibles con clientes antiguos), latencia extremadamente baja, reproducción 4K sin interrupciones
|
||||||
- Primer proveedor global con **protocolo QUIC**, ahora con protocolos de la familia QUIC más rápidos (ideal para el cliente Clash Verge)
|
- Primer proveedor global con **protocolo QUIC**, ahora con protocolos de la familia QUIC más rápidos (ideal para el cliente Clash Verge)
|
||||||
- Desbloquea servicios de streaming y acceso a ChatGPT
|
- Desbloquea servicios de streaming y acceso a ChatGPT
|
||||||
- Sitio oficial: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Sitio oficial: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
### 🤖 [GPTKefu — Plataforma de atención al cliente con IA integrada con Crisp](https://gptkefu.com)
|
### 🤖 [GPTKefu — Plataforma de atención al cliente con IA integrada con Crisp](https://gptkefu.com)
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -11,13 +11,14 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
زبانها:
|
زبانها:
|
||||||
<a href="../README.md">简体中文</a> ·
|
|
||||||
<a href="./README_en.md">English</a> ·
|
<a href="./README_en.md">English</a> ·
|
||||||
<a href="./README_es.md">Español</a> ·
|
<a href="./README_es.md">Español</a> ·
|
||||||
<a href="./README_ru.md">Русский</a> ·
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
<a href="./README_ja.md">日本語</a> ·
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
<a href="./README_ko.md">한국어</a> ·
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
<a href="./README_fa.md">فارسی</a>
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## پیشنمایش
|
## پیشنمایش
|
||||||
@@ -50,17 +51,17 @@
|
|||||||
|
|
||||||
## تبلیغات
|
## تبلیغات
|
||||||
|
|
||||||
#### [Doggygo VPN — شتابدهنده جهانی عملکردگرا](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
#### [Doggygo VPN — شتابدهنده جهانی عملکردگرا](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
- سرویس شبکه برون مرزی با عملکرد بالا به همراه دورههای آزمایشی رایگان، طرحهای تخفیفدار، امکان باز کردن قفل استریم و پشتیبانی درجه یک از پروتکل هیستریا.
|
- سرویس شبکه برون مرزی با عملکرد بالا به همراه دورههای آزمایشی رایگان، طرحهای تخفیفدار، امکان باز کردن قفل استریم و پشتیبانی درجه یک از پروتکل هیستریا.
|
||||||
- از طریق لینک اختصاصی Clash Verge ثبت نام کنید تا یک دوره آزمایشی ۳ روزه با ۱ گیگابایت ترافیک در روز دریافت کنید: [ثبت نام](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- از طریق لینک اختصاصی Clash Verge ثبت نام کنید تا یک دوره آزمایشی ۳ روزه با ۱ گیگابایت ترافیک در روز دریافت کنید: [ثبت نام](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
- کوپن تخفیف ۲۰٪ ویژه کاربران Clash Verge: `verge20` (محدود به ۵۰۰ بار استفاده)
|
- کوپن تخفیف ۲۰٪ ویژه کاربران Clash Verge: `verge20` (محدود به ۵۰۰ بار استفاده)
|
||||||
- بسته تخفیفدار از ۱۵.۸ ین در ماه برای ۱۶۰ گیگابایت، به علاوه ۲۰٪ تخفیف اضافی برای صورتحساب سالانه
|
- بسته تخفیفدار از ۱۵.۸ ین در ماه برای ۱۶۰ گیگابایت، به علاوه ۲۰٪ تخفیف اضافی برای صورتحساب سالانه
|
||||||
- توسط یک تیم خارجی با خدمات قابل اعتماد و تا 50٪ سهم درآمد اداره میشود
|
- توسط یک تیم خارجی با خدمات قابل اعتماد و تا 50٪ سهم درآمد اداره میشود
|
||||||
- کلاسترهای متعادل بار با مسیرهای اختصاصی پرسرعت (سازگار با کلاینتهای قدیمی)، تأخیر فوقالعاده کم، پخش روان 4K
|
- کلاسترهای متعادل بار با مسیرهای اختصاصی پرسرعت (سازگار با کلاینتهای قدیمی)، تأخیر فوقالعاده کم، پخش روان 4K
|
||||||
- اولین ارائهدهنده جهانی با **پروتکل QUIC**، اکنون با پروتکلهای سریعتر خانواده QUIC (بهترین ترکیب با کلاینت Clash Verge)
|
- اولین ارائهدهنده جهانی با **پروتکل QUIC**، اکنون با پروتکلهای سریعتر خانواده QUIC (بهترین ترکیب با کلاینت Clash Verge)
|
||||||
- پشتیبانی از سرویسهای استریم و دسترسی به ChatGPT
|
- پشتیبانی از سرویسهای استریم و دسترسی به ChatGPT
|
||||||
- وبسایت رسمی: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- وبسایت رسمی: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
### 🤖 [GPTKefu — پلتفرم خدمات مشتری هوشمند مبتنی بر هوش مصنوعی با ادغام عمیق Crisp](https://gptkefu.com)
|
### 🤖 [GPTKefu — پلتفرم خدمات مشتری هوشمند مبتنی بر هوش مصنوعی با ادغام عمیق Crisp](https://gptkefu.com)
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -11,13 +11,14 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
言語:
|
言語:
|
||||||
<a href="../README.md">简体中文</a> ·
|
|
||||||
<a href="./README_en.md">English</a> ·
|
<a href="./README_en.md">English</a> ·
|
||||||
<a href="./README_es.md">Español</a> ·
|
<a href="./README_es.md">Español</a> ·
|
||||||
<a href="./README_ru.md">Русский</a> ·
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
<a href="./README_ja.md">日本語</a> ·
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
<a href="./README_ko.md">한국어</a> ·
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
<a href="./README_fa.md">فارسی</a>
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## プレビュー
|
## プレビュー
|
||||||
@@ -51,17 +52,17 @@ Windows (x64/x86)、Linux (x64/arm64)、macOS 10.15+ (Intel/Apple) をサポー
|
|||||||
|
|
||||||
## プロモーション
|
## プロモーション
|
||||||
|
|
||||||
#### [Doggygo VPN — 高性能グローバルアクセラレータ](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
#### [Doggygo VPN — 高性能グローバルアクセラレータ](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
- 無料トライアル、割引プラン、ストリーミング解放、世界初の Hysteria プロトコル対応を備えた高性能海外ネットワークサービス。
|
- 無料トライアル、割引プラン、ストリーミング解放、世界初の Hysteria プロトコル対応を備えた高性能海外ネットワークサービス。
|
||||||
- Clash Verge 専用リンクから登録すると、3 日間・1 日 1 GB の無料体験が利用できます。 [登録はこちら](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Clash Verge 専用リンクから登録すると、3 日間・1 日 1 GB の無料体験が利用できます。 [登録はこちら](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
- Clash Verge 利用者限定 20% オフクーポン: `verge20`(先着 500 名)
|
- Clash Verge 利用者限定 20% オフクーポン: `verge20`(先着 500 名)
|
||||||
- 月額 15.8 元で 160 GB を利用できるプラン、年額契約ならさらに 20% オフ
|
- 月額 15.8 元で 160 GB を利用できるプラン、年額契約ならさらに 20% オフ
|
||||||
- 海外チーム運営による高信頼サービス、収益シェアは最大 50%
|
- 海外チーム運営による高信頼サービス、収益シェアは最大 50%
|
||||||
- 負荷分散クラスタと高速専用回線(旧クライアント互換)、極低レイテンシで 4K も快適
|
- 負荷分散クラスタと高速専用回線(旧クライアント互換)、極低レイテンシで 4K も快適
|
||||||
- 世界初の **QUIC プロトコル**対応。より高速な QUIC 系プロトコルを提供(Clash Verge クライアントとの相性抜群)
|
- 世界初の **QUIC プロトコル**対応。より高速な QUIC 系プロトコルを提供(Clash Verge クライアントとの相性抜群)
|
||||||
- ストリーミングおよび ChatGPT の利用にも対応
|
- ストリーミングおよび ChatGPT の利用にも対応
|
||||||
- 公式サイト: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- 公式サイト: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
### 🤖 [GPTKefu — Crisp と深く統合された AI スマートカスタマーサービスプラットフォーム](https://gptkefu.com)
|
### 🤖 [GPTKefu — Crisp と深く統合された AI スマートカスタマーサービスプラットフォーム](https://gptkefu.com)
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -11,13 +11,14 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
언어:
|
언어:
|
||||||
<a href="../README.md">简体中文</a> ·
|
|
||||||
<a href="./README_en.md">English</a> ·
|
<a href="./README_en.md">English</a> ·
|
||||||
<a href="./README_es.md">Español</a> ·
|
<a href="./README_es.md">Español</a> ·
|
||||||
<a href="./README_ru.md">Русский</a> ·
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
<a href="./README_ja.md">日本語</a> ·
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
<a href="./README_ko.md">한국어</a> ·
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
<a href="./README_fa.md">فارسی</a>
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## 미리보기
|
## 미리보기
|
||||||
@@ -51,17 +52,17 @@ Windows (x64/x86), Linux (x64/arm64), macOS 10.15+ (Intel/Apple)을 지원합니
|
|||||||
|
|
||||||
## 프로모션
|
## 프로모션
|
||||||
|
|
||||||
#### [Doggygo VPN — 고성능 글로벌 가속기](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
#### [Doggygo VPN — 고성능 글로벌 가속기](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
- 무료 체험, 할인 요금제, 스트리밍 해제, 선도적인 Hysteria 프로토콜 지원을 갖춘 고성능 해외 네트워크 서비스
|
- 무료 체험, 할인 요금제, 스트리밍 해제, 선도적인 Hysteria 프로토콜 지원을 갖춘 고성능 해외 네트워크 서비스
|
||||||
- Clash Verge 전용 초대 링크로 가입 시 3일간 매일 1GB 무료 체험 제공: [가입하기](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Clash Verge 전용 초대 링크로 가입 시 3일간 매일 1GB 무료 체험 제공: [가입하기](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
- Clash Verge 전용 20% 할인 코드: `verge20` (선착순 500회)
|
- Clash Verge 전용 20% 할인 코드: `verge20` (선착순 500회)
|
||||||
- 월 15.8위안부터 160GB 제공, 연간 결제 시 추가 20% 할인
|
- 월 15.8위안부터 160GB 제공, 연간 결제 시 추가 20% 할인
|
||||||
- 해외 팀 운영, 높은 신뢰성, 최대 50% 커미션
|
- 해외 팀 운영, 높은 신뢰성, 최대 50% 커미션
|
||||||
- 로드밸런싱 클러스터, 고속 전용 회선(구 클라이언트 호환), 매우 낮은 지연, 4K도 쾌적
|
- 로드밸런싱 클러스터, 고속 전용 회선(구 클라이언트 호환), 매우 낮은 지연, 4K도 쾌적
|
||||||
- 세계 최초 **QUIC 프로토콜** 지원, 더 빠른 QUIC 계열 프로토콜 제공 (Clash Verge 클라이언트와 최적의 궁합)
|
- 세계 최초 **QUIC 프로토콜** 지원, 더 빠른 QUIC 계열 프로토콜 제공 (Clash Verge 클라이언트와 최적의 궁합)
|
||||||
- 스트리밍 및 ChatGPT 접근 지원
|
- 스트리밍 및 ChatGPT 접근 지원
|
||||||
- 공식 사이트: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- 공식 사이트: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
### 🤖 [GPTKefu — Crisp과 긴밀히 통합된 AI 스마트 고객 서비스 플랫폼](https://gptkefu.com)
|
### 🤖 [GPTKefu — Crisp과 긴밀히 통합된 AI 스마트 고객 서비스 플랫폼](https://gptkefu.com)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<img src="../src-tauri/icons/icon.png" alt="Clash" width="128" />
|
||||||
|
<br>
|
||||||
|
Continuação do <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
|
||||||
|
<br>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h3 align="center">
|
||||||
|
Uma interface gráfica para o Clash Meta construída com <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Idiomas:
|
||||||
|
<a href="./README_en.md">English</a> ·
|
||||||
|
<a href="./README_es.md">Español</a> ·
|
||||||
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Pré-visualização
|
||||||
|
|
||||||
|
| Escuro | Claro |
|
||||||
|
| -------------------------------------- | -------------------------------------- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
Acesse a [página de lançamentos](https://github.com/clash-verge-rev/clash-verge-rev/releases) e baixe o instalador correspondente à sua plataforma.<br>
|
||||||
|
Oferecemos pacotes para Windows (x64/x86), Linux (x64/arm64) e macOS 10.15+ (Intel/Apple).
|
||||||
|
|
||||||
|
#### Como escolher o canal de lançamento
|
||||||
|
|
||||||
|
| Canal | Descrição | Link |
|
||||||
|
| :---------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- |
|
||||||
|
| Stable | Versões oficiais de alta confiabilidade, ideais para o uso diário. | [Release](https://github.com/clash-verge-rev/clash-verge-rev/releases) |
|
||||||
|
| Alpha (EOL) | Versões antigas usadas para validar o fluxo de publicação. | [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) |
|
||||||
|
| AutoBuild | Versões contínuas para testes e feedback. Espere mudanças típicas de beta. | [AutoBuild](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild) |
|
||||||
|
|
||||||
|
#### Guias de instalação e perguntas frequentes
|
||||||
|
|
||||||
|
Consulte a [documentação do projeto](https://clash-verge-rev.github.io/) para ver os passos de instalação, a solução de problemas e as perguntas frequentes.
|
||||||
|
|
||||||
|
### Canal do Telegram
|
||||||
|
|
||||||
|
Entre em [@clash_verge_rev](https://t.me/clash_verge_re) para acompanhar as novidades.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Promoções
|
||||||
|
|
||||||
|
#### [Doggygo VPN — Acelerador global focado em desempenho](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
- Serviço internacional de alto desempenho com teste gratuito, planos com desconto, desbloqueio de streaming e suporte de primeira linha ao protocolo Hysteria.
|
||||||
|
- Cadastre-se pelo link exclusivo do Clash Verge e ganhe um teste de 3 dias com 1 GB de tráfego diário: [Cadastre-se](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
- Cupom exclusivo de 20% de desconto para usuários do Clash Verge: `verge20` (limitado a 500 usos)
|
||||||
|
- Plano promocional a partir de ¥15,8 por mês com 160 GB, mais 20% de desconto adicional no pagamento anual
|
||||||
|
- Equipe sediada no exterior para um serviço confiável, com até 50% de comissão compartilhada
|
||||||
|
- Clusters balanceados com rotas dedicadas de alta velocidade (compatíveis com clientes antigos), latência extremamente baixa, reprodução 4K sem travamentos
|
||||||
|
- Primeiro provedor global com **protocolo QUIC**, agora com protocolos da família QUIC mais rápidos (ideal para o cliente Clash Verge)
|
||||||
|
- Desbloqueia serviços de streaming e acesso ao ChatGPT
|
||||||
|
- Site oficial: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
|
### 🤖 [GPTKefu — Plataforma de atendimento ao cliente com IA integrada ao Crisp](https://gptkefu.com)
|
||||||
|
|
||||||
|
- 🧠 Compreensão profunda de todo o contexto da conversa + reconhecimento de imagens, com respostas profissionais e precisas de forma automática, sem respostas robóticas.
|
||||||
|
- ♾️ **Respostas ilimitadas**, sem preocupação com cotas — ao contrário de outros produtos de IA que cobram por mensagem.
|
||||||
|
- 💬 Consultas de pré-venda, suporte pós-venda, resolução de problemas complexos — cobre todos os cenários com facilidade, com casos reais comprovados.
|
||||||
|
- ⚡ Configuração em 3 minutos, sem curva de aprendizado — melhora na hora a eficiência e a satisfação do cliente.
|
||||||
|
- 🎁 Teste gratuito de 14 dias do plano Premium — experimente antes de pagar: 👉 [Testar grátis](https://gptkefu.com)
|
||||||
|
- 📢 Canal do Telegram do atendimento com IA: [@crisp_ai](https://t.me/crisp_ai)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funcionalidades
|
||||||
|
|
||||||
|
- Baseado em Rust de alto desempenho e no framework Tauri 2
|
||||||
|
- Inclui o núcleo integrado [Clash.Meta (mihomo)](https://github.com/MetaCubeX/mihomo) e permite trocar para o canal `Alpha`
|
||||||
|
- Interface limpa e elegante, com controles de cor do tema, ícones de grupos de proxy/bandeja e `CSS Injection`
|
||||||
|
- Gerenciamento avançado de perfis (ferramentas Merge e Script) com sugestões de sintaxe para as configurações
|
||||||
|
- Controle do proxy do sistema, modo guardião e suporte a `TUN` (adaptador de rede virtual)
|
||||||
|
- Editores visuais para nós e regras
|
||||||
|
- Backup e sincronização via WebDAV
|
||||||
|
|
||||||
|
### Perguntas frequentes
|
||||||
|
|
||||||
|
Acesse a [página de FAQ](https://clash-verge-rev.github.io/faq/windows.html) para ver instruções específicas por plataforma.
|
||||||
|
|
||||||
|
### Doações
|
||||||
|
|
||||||
|
[Apoie o desenvolvimento do Clash Verge Rev](https://github.com/sponsors/clash-verge-rev)
|
||||||
|
|
||||||
|
## Desenvolvimento
|
||||||
|
|
||||||
|
Consulte o [CONTRIBUTING.md](../CONTRIBUTING.md) para ver as diretrizes de contribuição.
|
||||||
|
|
||||||
|
Depois de instalar todos os pré-requisitos do **Tauri**, execute o ambiente de desenvolvimento com:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pnpm i
|
||||||
|
pnpm run prebuild
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contribuições
|
||||||
|
|
||||||
|
Issues e pull requests são bem-vindos.
|
||||||
|
|
||||||
|
## Agradecimentos
|
||||||
|
|
||||||
|
O Clash Verge Rev é baseado nos projetos a seguir, ou se inspira neles:
|
||||||
|
|
||||||
|
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): Interface gráfica para o Clash baseada em Tauri. Compatível com Windows, macOS e Linux.
|
||||||
|
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Crie aplicativos de desktop menores, mais rápidos e mais seguros com um frontend web.
|
||||||
|
- [Dreamacro/clash](https://github.com/Dreamacro/clash): Túnel baseado em regras escrito em Go.
|
||||||
|
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): Túnel baseado em regras escrito em Go.
|
||||||
|
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): Interface do Clash para Windows e macOS.
|
||||||
|
- [vitejs/vite](https://github.com/vitejs/vite): Ferramentas de frontend de nova geração com uma experiência ultrarrápida.
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
Licença GPL-3.0. Consulte o [arquivo de licença](../LICENSE) para mais detalhes.
|
||||||
+7
-6
@@ -11,13 +11,14 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Языки:
|
Языки:
|
||||||
<a href="../README.md">简体中文</a> ·
|
|
||||||
<a href="./README_en.md">English</a> ·
|
<a href="./README_en.md">English</a> ·
|
||||||
<a href="./README_es.md">Español</a> ·
|
<a href="./README_es.md">Español</a> ·
|
||||||
<a href="./README_ru.md">Русский</a> ·
|
<a href="./README_fa.md">فارسی</a> ·
|
||||||
<a href="./README_ja.md">日本語</a> ·
|
<a href="./README_ja.md">日本語</a> ·
|
||||||
<a href="./README_ko.md">한국어</a> ·
|
<a href="./README_ko.md">한국어</a> ·
|
||||||
<a href="./README_fa.md">فارسی</a>
|
<a href="./README_pt.md">Português</a> ·
|
||||||
|
<a href="./README_ru.md">Русский</a> ·
|
||||||
|
<a href="../README.md">简体中文</a>
|
||||||
</p>
|
</p>
|
||||||
## Предпросмотр
|
## Предпросмотр
|
||||||
|
|
||||||
@@ -47,17 +48,17 @@ Clash Meta GUI базируется на <a href="https://github.com/tauri-apps/
|
|||||||
|
|
||||||
## Продвижение
|
## Продвижение
|
||||||
|
|
||||||
#### [Doggygo VPN —— технический VPN-сервис (айрпорт)](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
#### [Doggygo VPN —— технический VPN-сервис (айрпорт)](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
- Высокопроизводительный иностранный VPN-сервис (айрпорт) с бесплатным пробным периодом, выгодными тарифами, возможностью разблокировки потокового ТВ и первым в мире поддержкой протокола Hysteria.
|
- Высокопроизводительный иностранный VPN-сервис (айрпорт) с бесплатным пробным периодом, выгодными тарифами, возможностью разблокировки потокового ТВ и первым в мире поддержкой протокола Hysteria.
|
||||||
- Зарегистрируйтесь по эксклюзивной ссылке Clash Verge и получите 3 дня бесплатного использования, 1 Гб трафика в день: [регистрация](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Зарегистрируйтесь по эксклюзивной ссылке Clash Verge и получите 3 дня бесплатного использования, 1 Гб трафика в день: [регистрация](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
- Эксклюзивный промо-код на скидку 20% для Clash Verge: verge20 (только 500 штук)
|
- Эксклюзивный промо-код на скидку 20% для Clash Verge: verge20 (только 500 штук)
|
||||||
- Специальный тарифный план всего за 15,8 юаней в месяц, 160 Гб трафика, скидка 20% при оплате за год
|
- Специальный тарифный план всего за 15,8 юаней в месяц, 160 Гб трафика, скидка 20% при оплате за год
|
||||||
- Команда за рубежом, без риска побега, до 50% кэшбэка
|
- Команда за рубежом, без риска побега, до 50% кэшбэка
|
||||||
- Архитектура с балансировкойнагрузки, высокоскоростная выделенная линия (совместима со старыми клиентами), чрезвычайно низкая задержка, без проблем в часы пик, 4K видео загружается мгновенно
|
- Архитектура с балансировкойнагрузки, высокоскоростная выделенная линия (совместима со старыми клиентами), чрезвычайно низкая задержка, без проблем в часы пик, 4K видео загружается мгновенно
|
||||||
- Первый в мире VPN-сервис (айрпорт) на **протоколе QUIC**, теперь с более быстрыми протоколами семейства QUIC (лучшее сочетание с клиентом Clash Verge)
|
- Первый в мире VPN-сервис (айрпорт) на **протоколе QUIC**, теперь с более быстрыми протоколами семейства QUIC (лучшее сочетание с клиентом Clash Verge)
|
||||||
- Разблокировка потоковые сервисы и ChatGPT
|
- Разблокировка потоковые сервисы и ChatGPT
|
||||||
- Официальный сайт: [https://狗狗加速.com](https://verge.dginv.click/#/register?code=oaxsAGo6)
|
- Официальный сайт: [https://狗狗加速.com](https://cvr.dginv.click/#/register?code=oaxsAGo6)
|
||||||
|
|
||||||
### 🤖 [GPTKefu — AI-платформа умного обслуживания клиентов с глубокой интеграцией Crisp](https://gptkefu.com)
|
### 🤖 [GPTKefu — AI-платформа умного обслуживания клиентов с глубокой интеграцией Crisp](https://gptkefu.com)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,92 @@ import pluginUnusedImports from 'eslint-plugin-unused-imports'
|
|||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
const ipcBoundaryRules = ({
|
||||||
|
allowCommands = false,
|
||||||
|
allowInvoke = false,
|
||||||
|
allowEvents = false,
|
||||||
|
} = {}) => {
|
||||||
|
const paths = [
|
||||||
|
...(!allowInvoke
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: '@tauri-apps/api/core',
|
||||||
|
importNames: ['invoke'],
|
||||||
|
allowTypeImports: true,
|
||||||
|
message: 'Invoke backend commands through services/cmds.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(!allowEvents
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: '@tauri-apps/api/event',
|
||||||
|
importNames: ['listen', 'emit', 'emitTo', 'once'],
|
||||||
|
allowTypeImports: true,
|
||||||
|
message:
|
||||||
|
'The event bus owns Tauri event subscriptions and emissions.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
const patterns = allowCommands
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
regex: '(^|/)bindings(\\.ts)?$',
|
||||||
|
importNames: ['commands'],
|
||||||
|
allowTypeImports: true,
|
||||||
|
message:
|
||||||
|
'Only services/cmds may import generated command implementations.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const modules = [
|
||||||
|
...(!allowInvoke ? ['@tauri-apps/api/core'] : []),
|
||||||
|
...(!allowEvents ? ['@tauri-apps/api/event'] : []),
|
||||||
|
]
|
||||||
|
const syntax = modules.flatMap((name) => [
|
||||||
|
{
|
||||||
|
selector: `ImportExpression[source.value='${name}']`,
|
||||||
|
message:
|
||||||
|
'Use the owned IPC service boundary instead of dynamically importing Tauri APIs.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: `CallExpression[callee.name='require'][arguments.0.value='${name}']`,
|
||||||
|
message:
|
||||||
|
'Use the owned IPC service boundary instead of requiring Tauri APIs.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: `TSImportEqualsDeclaration[moduleReference.expression.value='${name}']`,
|
||||||
|
message: 'Use the owned IPC service boundary for Tauri APIs.',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
if (!allowCommands) {
|
||||||
|
syntax.push(
|
||||||
|
{
|
||||||
|
selector: 'ImportExpression[source.value=/bindings(\\.ts)?$/]',
|
||||||
|
message:
|
||||||
|
'Only services/cmds may load generated command implementations.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
"CallExpression[callee.name='require'][arguments.0.value=/bindings(\\.ts)?$/]",
|
||||||
|
message:
|
||||||
|
'Only services/cmds may load generated command implementations.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector:
|
||||||
|
'TSImportEqualsDeclaration[moduleReference.expression.value=/bindings(\\.ts)?$/]',
|
||||||
|
message:
|
||||||
|
'Only services/cmds may load generated command implementations.',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'no-restricted-imports': ['error', { paths, patterns }],
|
||||||
|
'no-restricted-syntax': ['error', ...syntax],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
pluginESx.configs['flat/restrict-to-es2022'],
|
pluginESx.configs['flat/restrict-to-es2022'],
|
||||||
{
|
{
|
||||||
@@ -162,4 +248,33 @@ export default defineConfig([
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.{ts,tsx,js,jsx,mts,mjs}'],
|
||||||
|
rules: ipcBoundaryRules(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/services/cmds.ts'],
|
||||||
|
rules: ipcBoundaryRules({ allowCommands: true }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/services/bindings.ts'],
|
||||||
|
rules: ipcBoundaryRules({ allowCommands: true, allowInvoke: true }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/services/bus/**/*.{ts,tsx}'],
|
||||||
|
rules: ipcBoundaryRules({ allowEvents: true }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'src/components/proxy/proxy-chain.tsx',
|
||||||
|
'src/components/proxy/proxy-groups-chain.tsx',
|
||||||
|
'src/pages/proxies.tsx',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-floating-promises': [
|
||||||
|
'error',
|
||||||
|
{ ignoreVoid: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/knip@6/schema.json",
|
"$schema": "https://unpkg.com/knip@6/schema.json",
|
||||||
"entry": ["src/polyfills/*.js!"],
|
"entry": ["src/polyfills/*.js!", "scripts/check-contract-changes.mjs!"],
|
||||||
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
|
"project": ["src/**/*.{js,jsx,ts,tsx,sass,scss}!", "scripts/**/*.{js,mjs}!"],
|
||||||
"include": [
|
"include": [
|
||||||
"files",
|
"files",
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"ignoreBinaries": ["rustc", "cc"],
|
"ignoreBinaries": ["rustc", "cc"],
|
||||||
"ignoreDependencies": ["lint-staged"],
|
"ignoreDependencies": ["lint-staged"],
|
||||||
"ignoreIssues": {
|
"ignoreIssues": {
|
||||||
"src/types/generated/**": ["exports", "types"]
|
"src/types/generated/**": ["exports", "types"],
|
||||||
|
"src/services/bindings.ts": ["types"]
|
||||||
},
|
},
|
||||||
"cycles": {
|
"cycles": {
|
||||||
"dynamicImports": true
|
"dynamicImports": true
|
||||||
|
|||||||
+54
-52
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "2.5.4",
|
"version": "2.5.9",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky || true",
|
"prepare": "husky || true",
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
"i18n:check": "node scripts/cleanup-unused-i18n.mjs",
|
"i18n:check": "node scripts/cleanup-unused-i18n.mjs",
|
||||||
"i18n:format": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
"i18n:format": "node scripts/cleanup-unused-i18n.mjs --align --apply",
|
||||||
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
"i18n:types": "node scripts/generate-i18n-keys.mjs",
|
||||||
|
"gen:contract": "node scripts/gen-contract.mjs",
|
||||||
|
"arch:check": "node scripts/check-architecture.mjs listen emit invoke",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
@@ -49,90 +51,90 @@
|
|||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@mui/icons-material": "^9.0.1",
|
"@mui/icons-material": "^9.4.0",
|
||||||
"@mui/material": "^9.0.1",
|
"@mui/material": "^9.4.0",
|
||||||
"@tanstack/react-virtual": "^3.13.25",
|
"@tanstack/react-virtual": "^3.14.13",
|
||||||
"@tauri-apps/api": "2.11.1",
|
"@tauri-apps/api": "3.0.0-alpha.2",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^3.0.0-alpha.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^3.0.0-alpha.3",
|
||||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
"@tauri-apps/plugin-fs": "^3.0.0-alpha.2",
|
||||||
"@tauri-apps/plugin-http": "~2.6.0",
|
"@tauri-apps/plugin-http": "~3.0.0-alpha.2",
|
||||||
"@tauri-apps/plugin-opener": "2.5.5",
|
"@tauri-apps/plugin-opener": "3.0.0-alpha.2",
|
||||||
"@tauri-apps/plugin-updater": "2.11.0",
|
"@tauri-apps/plugin-updater": "3.0.0-alpha.2",
|
||||||
"ahooks": "^3.9.7",
|
"ahooks": "^3.10.0",
|
||||||
"cidr-block": "^2.3.0",
|
"cidr-block": "^2.3.0",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
"dayjs": "1.11.23",
|
"dayjs": "1.11.23",
|
||||||
"foxact": "^0.3.4",
|
"foxact": "^0.3.12",
|
||||||
"foxts": "^5.4.2",
|
"foxts": "^5.9.1",
|
||||||
"i18next": "^26.2.0",
|
"i18next": "^26.4.2",
|
||||||
"js-yaml": "^5.0.0",
|
"js-yaml": "^5.4.2",
|
||||||
"lodash-es": "^4.18.1",
|
"lodash-es": "^4.18.1",
|
||||||
"meta-json-schema": "^1.19.25",
|
"meta-json-schema": "^1.19.31",
|
||||||
"monaco-editor": "^0.56.0",
|
"monaco-editor": "^0.57.0",
|
||||||
"monaco-yaml": "^5.5.0",
|
"monaco-yaml": "^5.5.1",
|
||||||
"nanoid": "^6.0.0",
|
"nanoid": "^6.0.1",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "19.3.0",
|
"react": "19.3.0",
|
||||||
"react-dom": "19.3.0",
|
"react-dom": "19.3.0",
|
||||||
"react-error-boundary": "6.1.5",
|
"react-error-boundary": "6.1.6",
|
||||||
"react-hook-form": "^7.76.1",
|
"react-hook-form": "^7.88.0",
|
||||||
"react-i18next": "17.0.14",
|
"react-i18next": "17.0.15",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router": "^8.0.0",
|
"react-router": "^8.4.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"swr": "^2.4.2",
|
"swr": "^2.5.1",
|
||||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
|
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main",
|
||||||
"types-pac": "^1.0.3",
|
"types-pac": "^1.0.3",
|
||||||
"validator": "^13.15.35"
|
"validator": "^13.15.35"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/github": "^9.1.1",
|
"@actions/github": "^9.1.1",
|
||||||
"@biomejs/biome": "^2.4.15",
|
"@biomejs/biome": "^2.5.14",
|
||||||
"@eslint-react/eslint-plugin": "^5.8.5",
|
"@eslint-react/eslint-plugin": "^5.20.5",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tauri-apps/cli": "2.11.4",
|
"@tauri-apps/cli": "3.0.0-alpha.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.12.4",
|
"@types/node": "^24.13.6",
|
||||||
"@types/react": "19.3.0",
|
"@types/react": "19.3.0",
|
||||||
"@types/react-dom": "19.3.0",
|
"@types/react-dom": "19.3.0",
|
||||||
"@types/validator": "^13.15.10",
|
"@types/validator": "^13.15.10",
|
||||||
"@vitejs/plugin-legacy": "^8.0.2",
|
"@vitejs/plugin-legacy": "^8.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.1.1",
|
||||||
"@wdio/tauri-service": "1.4.0",
|
"@wdio/tauri-service": "1.4.0",
|
||||||
"adm-zip": "^0.6.0",
|
"adm-zip": "^0.6.1",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.20.0",
|
||||||
"cli-color": "^2.0.4",
|
"cli-color": "^2.0.4",
|
||||||
"commander": "^15.0.0",
|
"commander": "^15.0.0",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^10.4.0",
|
"eslint": "^10.11.0",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.5",
|
||||||
"eslint-plugin-es-x": "^10.0.0",
|
"eslint-plugin-es-x": "^10.0.1",
|
||||||
"eslint-plugin-import-x": "^4.16.2",
|
"eslint-plugin-import-x": "^4.17.1",
|
||||||
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
"eslint-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.7",
|
||||||
"eslint-plugin-sukka": "^10.0.0",
|
"eslint-plugin-sukka": "^10.2.0",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"glob": "^13.0.6",
|
"glob": "^13.0.6",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.12.0",
|
||||||
"https-proxy-agent": "^9.0.0",
|
"https-proxy-agent": "^9.1.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.7.0",
|
"jiti": "^2.7.0",
|
||||||
"knip": "6.35.1",
|
"knip": "6.39.0",
|
||||||
"lint-staged": "^17.0.5",
|
"lint-staged": "^17.5.1",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.104.1",
|
||||||
"tar": "^7.5.15",
|
"tar": "^7.5.22",
|
||||||
"terser": "^5.48.0",
|
"terser": "^5.51.2",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.59.4",
|
"typescript-eslint": "^8.70.0",
|
||||||
"vite": "^8.0.14",
|
"vite": "^8.3.0",
|
||||||
"vite-plugin-svgr": "^5.2.0",
|
"vite-plugin-svgr": "^5.2.0",
|
||||||
"vitest": "^5.0.0",
|
"vitest": "^5.0.1",
|
||||||
"webdriverio": "9.31.9",
|
"webdriverio": "10.0.0",
|
||||||
"ws": "8.21.3"
|
"ws": "8.22.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,js,mjs}": [
|
"*.{ts,tsx,js,mjs}": [
|
||||||
@@ -144,5 +146,5 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@12.4.1"
|
"packageManager": "pnpm@12.11.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1403
-1547
File diff suppressed because it is too large
Load Diff
@@ -10,3 +10,15 @@ allowBuilds:
|
|||||||
unrs-resolver: true
|
unrs-resolver: true
|
||||||
minimumReleaseAgeExclude:
|
minimumReleaseAgeExclude:
|
||||||
- dayjs@1.11.21
|
- dayjs@1.11.21
|
||||||
|
- '@tauri-apps/cli-darwin-arm64@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-darwin-x64@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-linux-arm-gnueabihf@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-linux-arm64-gnu@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-linux-arm64-musl@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-linux-riscv64-gnu@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-linux-x64-gnu@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-linux-x64-musl@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-win32-arm64-msvc@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-win32-ia32-msvc@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli-win32-x64-msvc@3.0.0-alpha.4'
|
||||||
|
- '@tauri-apps/cli@3.0.0-alpha.4'
|
||||||
|
|||||||
+17
-1
@@ -56,6 +56,12 @@
|
|||||||
"matchFileNames": [".github/workflows/*.lock.yml"],
|
"matchFileNames": [".github/workflows/*.lock.yml"],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Ignore gh-aw dependency updates in workflow files; upgrade agentic workflows with gh aw upgrade instead",
|
||||||
|
"matchFileNames": [".github/workflows/**"],
|
||||||
|
"matchPackageNames": ["/gh-aw/"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Skip TypeScript 7.0 series",
|
"description": "Skip TypeScript 7.0 series",
|
||||||
"matchManagers": ["npm"],
|
"matchManagers": ["npm"],
|
||||||
@@ -78,7 +84,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Group PNPM version updates across package.json, tool-versions, workflows, and devcontainer",
|
"description": "Group PNPM version updates across package.json, tool-versions, workflows, and devcontainer",
|
||||||
"matchPackageNames": ["pnpm", "ghcr.io/devcontainers/features/pnpm"],
|
"matchPackageNames": ["pnpm"],
|
||||||
"groupName": "pnpm"
|
"groupName": "pnpm"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -131,6 +137,16 @@
|
|||||||
],
|
],
|
||||||
"depNameTemplate": "rust-lang/rust",
|
"depNameTemplate": "rust-lang/rust",
|
||||||
"datasourceTemplate": "github-releases"
|
"datasourceTemplate": "github-releases"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Update pnpm in devcontainer.json",
|
||||||
|
"fileMatch": ["^\\.devcontainer/devcontainer\\.json$"],
|
||||||
|
"matchStrings": [
|
||||||
|
"\"ghcr\\.io/devcontainers/features/node:[^\"]+\":\\s*\\{[^}]*\"pnpmVersion\":\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "pnpm",
|
||||||
|
"datasourceTemplate": "npm"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"postUpdateOptions": ["pnpmDedupe"],
|
"postUpdateOptions": ["pnpmDedupe"],
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.98.1"
|
channel = "1.99.0"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { ESLint } from 'eslint'
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dirname, '..')
|
||||||
|
const gates = process.argv.slice(2)
|
||||||
|
if (
|
||||||
|
!gates.length ||
|
||||||
|
gates.some((gate) => !['listen', 'emit', 'invoke'].includes(gate))
|
||||||
|
) {
|
||||||
|
console.error('usage: check-architecture.mjs <gate>... (listen|emit|invoke)')
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All IPC boundaries use the lint configuration, including when callers request a legacy gate.
|
||||||
|
const eslint = new ESLint({ cwd: ROOT, allowInlineConfig: false })
|
||||||
|
const results = await eslint.lintFiles(['src'])
|
||||||
|
const boundaryRules = new Set([
|
||||||
|
'no-restricted-imports',
|
||||||
|
'no-restricted-syntax',
|
||||||
|
'@typescript-eslint/no-floating-promises',
|
||||||
|
])
|
||||||
|
const violations = results.flatMap((result) =>
|
||||||
|
result.messages
|
||||||
|
.filter((message) => message.fatal || boundaryRules.has(message.ruleId))
|
||||||
|
.map((message) => ({ file: result.filePath, ...message })),
|
||||||
|
)
|
||||||
|
for (const violation of violations) {
|
||||||
|
console.error(
|
||||||
|
`[arch] ${path.relative(ROOT, violation.file)}:${violation.line}: ${violation.message}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (violations.length) process.exitCode = 1
|
||||||
|
else console.log('[arch] no violations')
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dirname, '..')
|
||||||
|
const git = (...args) =>
|
||||||
|
execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }).trim()
|
||||||
|
const { contract } = JSON.parse(
|
||||||
|
fs.readFileSync(path.join(ROOT, 'scripts/contract-inputs.json'), 'utf8'),
|
||||||
|
)
|
||||||
|
const inputs = contract.map((pattern) => `:(glob)${pattern}`)
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
if (args.length !== 2 || args[0] !== '--push') {
|
||||||
|
console.error('usage: check-contract-changes.mjs --push <remote>')
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
const hasChanges = (...range) =>
|
||||||
|
git('diff', '--name-only', ...range, '--', ...inputs) !== ''
|
||||||
|
const assertCleanInputs = (...range) => {
|
||||||
|
if (
|
||||||
|
hasChanges(...range) ||
|
||||||
|
git('ls-files', '--others', '--exclude-standard', '--', ...inputs)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Contract inputs differ from the pushed commit. Commit those changes before pushing.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let relevant = false
|
||||||
|
const head = git('rev-parse', 'HEAD')
|
||||||
|
for (const line of fs
|
||||||
|
.readFileSync(0, 'utf8')
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter(Boolean)) {
|
||||||
|
const fields = line.trim().split(/\s+/)
|
||||||
|
if (
|
||||||
|
fields.length !== 4 ||
|
||||||
|
![fields[1], fields[3]].every((sha) => /^[a-f0-9]{40,64}$/.test(sha))
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid pre-push ref update')
|
||||||
|
}
|
||||||
|
const [, local, , remote] = fields
|
||||||
|
if (/^0+$/.test(local) || local === remote) continue
|
||||||
|
// A new remote branch has no upstream; use its fork point with the remote default branch.
|
||||||
|
const base = /^0+$/.test(remote)
|
||||||
|
? git('merge-base', local, `refs/remotes/${args[1]}/HEAD`)
|
||||||
|
: remote
|
||||||
|
if (!hasChanges(base, local)) continue
|
||||||
|
const commit = git('rev-parse', `${local}^{commit}`)
|
||||||
|
if (commit !== head)
|
||||||
|
throw new Error(
|
||||||
|
'Check out the pushed commit before validating its changed IPC contract.',
|
||||||
|
)
|
||||||
|
assertCleanInputs('HEAD')
|
||||||
|
relevant = true
|
||||||
|
}
|
||||||
|
if (relevant) {
|
||||||
|
execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[path.join(ROOT, 'scripts/gen-contract.mjs'), '--check'],
|
||||||
|
{
|
||||||
|
cwd: ROOT,
|
||||||
|
stdio: 'inherit',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.log('[contract] inputs unchanged; skipping Rust generation')
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"contract": [
|
||||||
|
"src-tauri/**/*.rs",
|
||||||
|
"crates/**/*.rs",
|
||||||
|
"Cargo.toml",
|
||||||
|
"Cargo.lock",
|
||||||
|
"src-tauri/Cargo.toml",
|
||||||
|
"crates/**/Cargo.toml",
|
||||||
|
"rust-toolchain.toml",
|
||||||
|
"src-tauri/tauri.conf*.json",
|
||||||
|
"src/services/bindings.ts",
|
||||||
|
"scripts/gen-contract.mjs",
|
||||||
|
"scripts/check-contract-changes.mjs",
|
||||||
|
"biome.json",
|
||||||
|
"biome.jsonc",
|
||||||
|
"package.json",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"pnpm-workspace.yaml",
|
||||||
|
"scripts/contract-inputs.json",
|
||||||
|
".github/workflows/frontend-check.yml",
|
||||||
|
".husky/pre-push"
|
||||||
|
]
|
||||||
|
}
|
||||||
+86
-142
@@ -1,36 +1,23 @@
|
|||||||
import { spawn } from 'node:child_process'
|
import { execFileSync, spawn } from 'node:child_process'
|
||||||
import { createHash } from 'node:crypto'
|
import { access } from 'node:fs/promises'
|
||||||
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
||||||
import { dirname, join, resolve } from 'node:path'
|
import { dirname, join, resolve } from 'node:path'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
export const developmentServiceDirectoryEnvironment =
|
export const developmentServiceDirectoryEnvironment =
|
||||||
'CLASH_VERGE_DEV_SERVICE_DIR'
|
'CLASH_VERGE_DEV_SERVICE_DIR'
|
||||||
const developmentServiceInstallerEnvironment =
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
'CLASH_VERGE_DEV_SERVICE_INSTALLER'
|
|
||||||
|
|
||||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url))
|
|
||||||
const repositoryRoot = resolve(scriptDirectory, '..')
|
|
||||||
const serviceRepository = resolve(
|
const serviceRepository = resolve(
|
||||||
repositoryRoot,
|
repositoryRoot,
|
||||||
'..',
|
'..',
|
||||||
'clash-verge-service-ipc',
|
'clash-verge-service-ipc',
|
||||||
)
|
)
|
||||||
const serviceManifest = join(serviceRepository, 'Cargo.toml')
|
const serviceManifest = join(serviceRepository, 'Cargo.toml')
|
||||||
const developmentReceipt = join(
|
export const developmentServiceWatchPaths = [
|
||||||
repositoryRoot,
|
'src',
|
||||||
'target',
|
'resources',
|
||||||
'development-service.json',
|
'Cargo.toml',
|
||||||
)
|
'Cargo.lock',
|
||||||
|
].map((name) => join(serviceRepository, name))
|
||||||
function executable(name, platform) {
|
|
||||||
return join(
|
|
||||||
serviceRepository,
|
|
||||||
'target',
|
|
||||||
'debug',
|
|
||||||
platform === 'win32' ? `${name}.exe` : name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function run(command, args, options = {}) {
|
function run(command, args, options = {}) {
|
||||||
return new Promise((resolvePromise, reject) => {
|
return new Promise((resolvePromise, reject) => {
|
||||||
@@ -39,10 +26,15 @@ function run(command, args, options = {}) {
|
|||||||
stdio: options.stdio ?? 'inherit',
|
stdio: options.stdio ?? 'inherit',
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
})
|
})
|
||||||
|
let stdout = ''
|
||||||
|
child.stdout?.setEncoding('utf8')
|
||||||
|
child.stdout?.on('data', (chunk) => {
|
||||||
|
stdout += chunk
|
||||||
|
})
|
||||||
child.once('error', reject)
|
child.once('error', reject)
|
||||||
child.once('exit', (code, signal) => {
|
child.once('close', (code, signal) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolvePromise()
|
resolvePromise(stdout)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
@@ -54,134 +46,86 @@ function run(command, args, options = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hashFiles(paths) {
|
export async function prepareDevelopmentService() {
|
||||||
const hash = createHash('sha256')
|
|
||||||
for (const path of paths) {
|
|
||||||
hash.update(await readFile(path))
|
|
||||||
}
|
|
||||||
return hash.digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readInstalledFingerprint() {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(await readFile(developmentReceipt, 'utf8'))
|
|
||||||
return typeof parsed.fingerprint === 'string' ? parsed.fingerprint : null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeInstalledFingerprint(fingerprint) {
|
|
||||||
await mkdir(dirname(developmentReceipt), { recursive: true })
|
|
||||||
await writeFile(
|
|
||||||
developmentReceipt,
|
|
||||||
`${JSON.stringify({ fingerprint }, null, 2)}\n`,
|
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function windowsElevationInvocation(installer, environment = process.env) {
|
|
||||||
const script = [
|
|
||||||
"$ErrorActionPreference = 'Stop'",
|
|
||||||
`$installer = [Environment]::GetEnvironmentVariable('${developmentServiceInstallerEnvironment}', 'Process')`,
|
|
||||||
"if ([string]::IsNullOrWhiteSpace($installer)) { throw 'Development service installer path is missing' }",
|
|
||||||
'$child = Start-Process -FilePath $installer -Verb RunAs -Wait -PassThru -WindowStyle Hidden',
|
|
||||||
'exit $child.ExitCode',
|
|
||||||
].join('; ')
|
|
||||||
return {
|
|
||||||
command: 'powershell.exe',
|
|
||||||
args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
||||||
env: {
|
|
||||||
...environment,
|
|
||||||
[developmentServiceInstallerEnvironment]: installer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function elevateInstaller(installer, platform) {
|
|
||||||
if (platform === 'win32') {
|
|
||||||
const invocation = windowsElevationInvocation(installer)
|
|
||||||
return run(invocation.command, invocation.args, { env: invocation.env })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platform === 'darwin') {
|
|
||||||
const script =
|
|
||||||
'on run argv\nset toolPath to item 1 of argv\nset groupId to item 2 of argv\ndo shell script "CLASH_VERGE_SERVICE_GID=" & quoted form of groupId & " " & quoted form of toolPath with administrator privileges\nend run'
|
|
||||||
return run('osascript', ['-e', script, installer, String(process.getgid())])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof process.getuid === 'function' && process.getuid() === 0) {
|
|
||||||
return run(installer, [], {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
CLASH_VERGE_SERVICE_GID: String(process.getgid()),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await run('pkexec', [installer])
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== 'ENOENT' && error.exitCode !== 127) throw error
|
|
||||||
await run('sudo', [installer])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function prepareDevelopmentService({
|
|
||||||
platform = process.platform,
|
|
||||||
} = {}) {
|
|
||||||
await access(serviceManifest)
|
await access(serviceManifest)
|
||||||
await run('cargo', [
|
const output = await run(
|
||||||
'build',
|
'cargo',
|
||||||
'--manifest-path',
|
[
|
||||||
serviceManifest,
|
'build',
|
||||||
'--features',
|
'--manifest-path',
|
||||||
'standalone,client,development-channel',
|
serviceManifest,
|
||||||
'--bins',
|
'--target-dir',
|
||||||
])
|
process.env.CARGO_TARGET_DIR ||
|
||||||
|
join(serviceRepository, 'target', 'development'),
|
||||||
|
'--features',
|
||||||
|
'standalone,client,development-channel',
|
||||||
|
'--bins',
|
||||||
|
'--message-format=json-render-diagnostics',
|
||||||
|
],
|
||||||
|
{ stdio: ['inherit', 'pipe', 'inherit'] },
|
||||||
|
)
|
||||||
|
|
||||||
const service = executable('clash-verge-service', platform)
|
const artifacts = new Map()
|
||||||
await access(service)
|
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
||||||
return dirname(service)
|
const artifact = JSON.parse(line)
|
||||||
|
if (
|
||||||
|
artifact.reason === 'compiler-artifact' &&
|
||||||
|
resolve(artifact.manifest_path) === serviceManifest &&
|
||||||
|
artifact.target.kind.includes('bin') &&
|
||||||
|
artifact.executable
|
||||||
|
) {
|
||||||
|
artifacts.set(artifact.target.name, resolve(artifact.executable))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const executables = [
|
||||||
|
'clash-verge-service',
|
||||||
|
'clash-verge-service-install',
|
||||||
|
'clash-verge-service-uninstall',
|
||||||
|
'service-integration-driver',
|
||||||
|
].map((name) => {
|
||||||
|
const executable = artifacts.get(name)
|
||||||
|
if (!executable)
|
||||||
|
throw new Error(`Cargo did not report the ${name} executable`)
|
||||||
|
return executable
|
||||||
|
})
|
||||||
|
const serviceDirectory = dirname(executables[0])
|
||||||
|
for (const executable of executables) {
|
||||||
|
if (dirname(executable) !== serviceDirectory) {
|
||||||
|
throw new Error(
|
||||||
|
'Development service tools were built in different directories',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await access(executable)
|
||||||
|
}
|
||||||
|
return serviceDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureDevelopmentService({
|
export async function ensureDevelopmentService({
|
||||||
platform = process.platform,
|
platform = process.platform,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const serviceDirectory = await prepareDevelopmentService({ platform })
|
const serviceDirectory = await prepareDevelopmentService()
|
||||||
const service = join(
|
const extension = platform === 'win32' ? '.exe' : ''
|
||||||
serviceDirectory,
|
|
||||||
platform === 'win32' ? 'clash-verge-service.exe' : 'clash-verge-service',
|
|
||||||
)
|
|
||||||
const installer = join(
|
const installer = join(
|
||||||
serviceDirectory,
|
serviceDirectory,
|
||||||
platform === 'win32'
|
`clash-verge-service-install${extension}`,
|
||||||
? 'clash-verge-service-install.exe'
|
|
||||||
: 'clash-verge-service-install',
|
|
||||||
)
|
)
|
||||||
const driver = join(
|
const host = execFileSync('rustc', ['-vV'], { encoding: 'utf8' }).match(
|
||||||
serviceDirectory,
|
/^host: (.+)$/m,
|
||||||
platform === 'win32'
|
)?.[1]
|
||||||
? 'service-integration-driver.exe'
|
if (!host) throw new Error('rustc did not report its host target')
|
||||||
: 'service-integration-driver',
|
const args = ['--prepare-install', '--ensure']
|
||||||
)
|
for (const name of ['verge-mihomo', 'verge-mihomo-alpha']) {
|
||||||
const fingerprint = await hashFiles([service, installer])
|
args.push(
|
||||||
const installedFingerprint = await readInstalledFingerprint()
|
'--core',
|
||||||
|
`${name}${extension}`,
|
||||||
if (installedFingerprint === fingerprint) {
|
join(
|
||||||
try {
|
repositoryRoot,
|
||||||
await run(driver, ['probe'], { stdio: 'ignore' })
|
'src-tauri',
|
||||||
return serviceDirectory
|
'sidecar',
|
||||||
} catch {
|
`${name}-${host}${extension}`,
|
||||||
// The expected binary is installed but unavailable; the repair tool owns recovery.
|
),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
await run(installer, args)
|
||||||
try {
|
|
||||||
await elevateInstaller(installer, platform)
|
|
||||||
} catch (error) {
|
|
||||||
if (error.exitCode !== 75) throw error
|
|
||||||
}
|
|
||||||
await run(driver, ['ready'])
|
|
||||||
await writeInstalledFingerprint(fingerprint)
|
|
||||||
return serviceDirectory
|
return serviceDirectory
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from './dev-control.mjs'
|
} from './dev-control.mjs'
|
||||||
import {
|
import {
|
||||||
developmentServiceDirectoryEnvironment,
|
developmentServiceDirectoryEnvironment,
|
||||||
|
developmentServiceWatchPaths,
|
||||||
ensureDevelopmentService,
|
ensureDevelopmentService,
|
||||||
prepareDevelopmentService,
|
prepareDevelopmentService,
|
||||||
} from './dev-service.mjs'
|
} from './dev-service.mjs'
|
||||||
@@ -31,6 +32,9 @@ export function buildTauriInvocation(
|
|||||||
env.RUSTFLAGS = '--cfg tokio_unstable'
|
env.RUSTFLAGS = '--cfg tokio_unstable'
|
||||||
args.push('tokio-trace')
|
args.push('tokio-trace')
|
||||||
}
|
}
|
||||||
|
for (const path of developmentServiceWatchPaths) {
|
||||||
|
args.push('--additional-watch-folders', path)
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
command: 'pnpm',
|
command: 'pnpm',
|
||||||
args,
|
args,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
const ROOT = path.resolve(import.meta.dirname, '..')
|
||||||
|
const BINDINGS = path.join(ROOT, 'src/services/bindings.ts')
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
if (args.length > 1 || (args.length === 1 && args[0] !== '--check')) {
|
||||||
|
console.error('usage: gen-contract.mjs [--check]')
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
const check = args[0] === '--check'
|
||||||
|
const temporary = check
|
||||||
|
? fs.mkdtempSync(path.join(os.tmpdir(), 'verge-contract-'))
|
||||||
|
: null
|
||||||
|
const output = temporary ? path.join(temporary, 'bindings.ts') : BINDINGS
|
||||||
|
try {
|
||||||
|
execFileSync(
|
||||||
|
'cargo',
|
||||||
|
[
|
||||||
|
'run',
|
||||||
|
'-p',
|
||||||
|
'clash-verge',
|
||||||
|
'--bin',
|
||||||
|
'frontend-contract',
|
||||||
|
'--locked',
|
||||||
|
'-q',
|
||||||
|
...(check ? ['--', output] : []),
|
||||||
|
],
|
||||||
|
{ cwd: ROOT, stdio: 'inherit' },
|
||||||
|
)
|
||||||
|
const formatted = execFileSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
|
||||||
|
'format',
|
||||||
|
'--stdin-file-path',
|
||||||
|
BINDINGS,
|
||||||
|
],
|
||||||
|
{ cwd: ROOT, input: fs.readFileSync(output) },
|
||||||
|
)
|
||||||
|
if (check) {
|
||||||
|
if (!formatted.equals(fs.readFileSync(BINDINGS))) {
|
||||||
|
console.error(
|
||||||
|
'IPC bindings are out of date. Run pnpm gen:contract and commit the generated file.',
|
||||||
|
)
|
||||||
|
process.exitCode = 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs.writeFileSync(BINDINGS, formatted)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (temporary) fs.rmSync(temporary, { recursive: true, force: true })
|
||||||
|
}
|
||||||
+53
-11
@@ -1,9 +1,9 @@
|
|||||||
import { execFileSync, execSync } from 'child_process'
|
import { execFileSync, execSync } from 'node:child_process'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import fs from 'fs'
|
import fs from 'node:fs'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'node:fs/promises'
|
||||||
import path from 'path'
|
import path from 'node:path'
|
||||||
import zlib from 'zlib'
|
import zlib from 'node:zlib'
|
||||||
|
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { glob } from 'glob'
|
import { glob } from 'glob'
|
||||||
@@ -485,7 +485,7 @@ const resolvePlugin = async () => {
|
|||||||
const zip = new AdmZip(tempZip)
|
const zip = new AdmZip(tempZip)
|
||||||
zip
|
zip
|
||||||
.getEntries()
|
.getEntries()
|
||||||
.forEach((entry) => log_debug(`"SimpleSC" entry`, entry.entryName))
|
.forEach((entry) => void log_debug(`"SimpleSC" entry`, entry.entryName))
|
||||||
zip.extractAllTo(tempDir, true)
|
zip.extractAllTo(tempDir, true)
|
||||||
if (fs.existsSync(tempDll)) {
|
if (fs.existsSync(tempDll)) {
|
||||||
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
|
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
|
||||||
@@ -553,7 +553,7 @@ const SERVICE_BINARIES = [
|
|||||||
|
|
||||||
function serviceFileInfo(name) {
|
function serviceFileInfo(name) {
|
||||||
const ext = platform === 'win32' ? '.exe' : ''
|
const ext = platform === 'win32' ? '.exe' : ''
|
||||||
const suffix = platform === 'linux' ? '-' + SIDECAR_HOST : ''
|
const suffix = platform === 'linux' ? `-${SIDECAR_HOST}` : ''
|
||||||
return {
|
return {
|
||||||
sourceFile: `${name}${ext}`,
|
sourceFile: `${name}${ext}`,
|
||||||
targetFile: `${name}${suffix}${ext}`,
|
targetFile: `${name}${suffix}${ext}`,
|
||||||
@@ -586,6 +586,41 @@ async function resolveServiceBundle() {
|
|||||||
path.join(cwd, 'src-tauri', 'Cargo.toml'),
|
path.join(cwd, 'src-tauri', 'Cargo.toml'),
|
||||||
'utf8',
|
'utf8',
|
||||||
)
|
)
|
||||||
|
const serviceDependency = cargoManifest
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.find((line) => line.trimStart().startsWith('clash_verge_service_ipc ='))
|
||||||
|
const sourcePath = serviceDependency?.match(/\bpath\s*=\s*"([^"]+)"/)?.[1]
|
||||||
|
if (sourcePath) {
|
||||||
|
const manifest = path.resolve(cwd, 'src-tauri', sourcePath, 'Cargo.toml')
|
||||||
|
const targetDirectory = path.join(cwd, 'target', 'bundled-service')
|
||||||
|
execFileSync(
|
||||||
|
'cargo',
|
||||||
|
[
|
||||||
|
'build',
|
||||||
|
'--manifest-path',
|
||||||
|
manifest,
|
||||||
|
'--target-dir',
|
||||||
|
targetDirectory,
|
||||||
|
'--target',
|
||||||
|
SIDECAR_HOST,
|
||||||
|
'--release',
|
||||||
|
'--features',
|
||||||
|
'standalone,client',
|
||||||
|
'--bins',
|
||||||
|
],
|
||||||
|
{ stdio: 'inherit' },
|
||||||
|
)
|
||||||
|
await fsp.mkdir(SERVICE_DIR, { recursive: true })
|
||||||
|
for (const { sourceFile, targetPath } of files) {
|
||||||
|
await fsp.copyFile(
|
||||||
|
path.join(targetDirectory, SIDECAR_HOST, 'release', sourceFile),
|
||||||
|
targetPath,
|
||||||
|
)
|
||||||
|
if (platform !== 'win32') await fsp.chmod(targetPath, 0o755)
|
||||||
|
await updateHashCache(targetPath)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
const { archiveFile, downloadURL } = resolveServiceRelease(
|
const { archiveFile, downloadURL } = resolveServiceRelease(
|
||||||
cargoManifest,
|
cargoManifest,
|
||||||
SIDECAR_HOST,
|
SIDECAR_HOST,
|
||||||
@@ -604,8 +639,9 @@ async function resolveServiceBundle() {
|
|||||||
const zip = new AdmZip(tempArchive)
|
const zip = new AdmZip(tempArchive)
|
||||||
zip
|
zip
|
||||||
.getEntries()
|
.getEntries()
|
||||||
.forEach((entry) =>
|
.forEach(
|
||||||
log_debug('"clash-verge-service-ipc" entry:', entry.entryName),
|
(entry) =>
|
||||||
|
void log_debug('"clash-verge-service-ipc" entry:', entry.entryName),
|
||||||
)
|
)
|
||||||
zip.extractAllTo(tempDir, true)
|
zip.extractAllTo(tempDir, true)
|
||||||
} else {
|
} else {
|
||||||
@@ -652,7 +688,7 @@ async function resolveCoreHashes() {
|
|||||||
.digest('hex')
|
.digest('hex')
|
||||||
lines.push(`!define ${define} "${digest}"`)
|
lines.push(`!define ${define} "${digest}"`)
|
||||||
}
|
}
|
||||||
await fsp.writeFile(CORE_HASHES_NSH, lines.join('\n') + '\n')
|
await fsp.writeFile(CORE_HASHES_NSH, `${lines.join('\n')}\n`)
|
||||||
log_success(`Generated ${CORE_HASHES_NSH}`)
|
log_success(`Generated ${CORE_HASHES_NSH}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,6 +697,11 @@ const resolveMmdb = () =>
|
|||||||
file: 'Country.mmdb',
|
file: 'Country.mmdb',
|
||||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
|
||||||
})
|
})
|
||||||
|
const resolveASNMmdb = () =>
|
||||||
|
resolveResource({
|
||||||
|
file: 'ASN.mmdb',
|
||||||
|
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb`,
|
||||||
|
})
|
||||||
const resolveGeosite = () =>
|
const resolveGeosite = () =>
|
||||||
resolveResource({
|
resolveResource({
|
||||||
file: 'geosite.dat',
|
file: 'geosite.dat',
|
||||||
@@ -706,6 +747,7 @@ const tasks = [
|
|||||||
{ name: 'plugin', func: resolvePlugin, retry: 5, winOnly: true },
|
{ name: 'plugin', func: resolvePlugin, retry: 5, winOnly: true },
|
||||||
{ name: 'service', func: resolveServiceBundle, retry: 5 },
|
{ name: 'service', func: resolveServiceBundle, retry: 5 },
|
||||||
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
|
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
|
||||||
|
{ name: 'asn_mmdb', func: resolveASNMmdb, retry: 5 },
|
||||||
{ name: 'geosite', func: resolveGeosite, retry: 5 },
|
{ name: 'geosite', func: resolveGeosite, retry: 5 },
|
||||||
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
|
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
|
||||||
{
|
{
|
||||||
|
|||||||
+50
-15
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, and src-tauri/tauri.conf.json.
|
* CLI tool to update version numbers in package.json, src-tauri/Cargo.toml, Cargo.lock, and src-tauri/tauri.conf.json.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* pnpm release-version <version>
|
* pnpm release-version <version>
|
||||||
@@ -108,17 +108,20 @@ async function updatePackageVersion(newVersion) {
|
|||||||
'[INFO]: Current package.json version is: ',
|
'[INFO]: Current package.json version is: ',
|
||||||
packageJson.version,
|
packageJson.version,
|
||||||
)
|
)
|
||||||
packageJson.version = newVersion.startsWith('v')
|
const versionWithoutV = newVersion.startsWith('v')
|
||||||
? newVersion.slice(1)
|
? newVersion.slice(1)
|
||||||
: newVersion
|
: newVersion
|
||||||
await fs.writeFile(
|
const updatedData = data.replace(
|
||||||
packageJsonPath,
|
/^(\s*"version"\s*:\s*)"[^"]+"/m,
|
||||||
JSON.stringify(packageJson, null, 2),
|
`$1"${versionWithoutV}"`,
|
||||||
'utf8',
|
|
||||||
)
|
|
||||||
console.log(
|
|
||||||
`[INFO]: package.json version updated to: ${packageJson.version}`,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (updatedData === data) {
|
||||||
|
throw new Error('version field was not found in package.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(packageJsonPath, updatedData, 'utf8')
|
||||||
|
console.log(`[INFO]: package.json version updated to: ${versionWithoutV}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating package.json version:', error)
|
console.error('Error updating package.json version:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -153,6 +156,34 @@ async function updateCargoVersion(newVersion) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateCargoLockVersion(newVersion) {
|
||||||
|
const _dirname = process.cwd()
|
||||||
|
const cargoLockPath = path.join(_dirname, 'Cargo.lock')
|
||||||
|
const versionWithoutV = newVersion.startsWith('v')
|
||||||
|
? newVersion.slice(1)
|
||||||
|
: newVersion
|
||||||
|
const packageVersionPattern =
|
||||||
|
/(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = )"[^"]+"/
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(cargoLockPath, 'utf8')
|
||||||
|
const updatedData = data.replace(
|
||||||
|
packageVersionPattern,
|
||||||
|
`$1"${versionWithoutV}"`,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updatedData === data) {
|
||||||
|
throw new Error('clash-verge package entry was not found in Cargo.lock')
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(cargoLockPath, updatedData, 'utf8')
|
||||||
|
console.log(`[INFO]: Cargo.lock version updated to: ${versionWithoutV}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating Cargo.lock version:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateTauriConfigVersion(newVersion) {
|
async function updateTauriConfigVersion(newVersion) {
|
||||||
const _dirname = process.cwd()
|
const _dirname = process.cwd()
|
||||||
const tauriConfigPath = path.join(_dirname, 'src-tauri', 'tauri.conf.json')
|
const tauriConfigPath = path.join(_dirname, 'src-tauri', 'tauri.conf.json')
|
||||||
@@ -168,13 +199,16 @@ async function updateTauriConfigVersion(newVersion) {
|
|||||||
tauriConfig.version,
|
tauriConfig.version,
|
||||||
)
|
)
|
||||||
|
|
||||||
tauriConfig.version = versionWithoutV
|
const updatedData = data.replace(
|
||||||
|
/^(\s*"version"\s*:\s*)"[^"]+"/m,
|
||||||
await fs.writeFile(
|
`$1"${versionWithoutV}"`,
|
||||||
tauriConfigPath,
|
|
||||||
JSON.stringify(tauriConfig, null, 2),
|
|
||||||
'utf8',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (updatedData === data) {
|
||||||
|
throw new Error('version field was not found in tauri.conf.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.writeFile(tauriConfigPath, updatedData, 'utf8')
|
||||||
console.log(
|
console.log(
|
||||||
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
`[INFO]: tauri.conf.json version updated to: ${versionWithoutV}`,
|
||||||
)
|
)
|
||||||
@@ -239,6 +273,7 @@ async function main(versionArg) {
|
|||||||
console.log(`[INFO]: Updating versions to: ${newVersion}`)
|
console.log(`[INFO]: Updating versions to: ${newVersion}`)
|
||||||
await updatePackageVersion(newVersion)
|
await updatePackageVersion(newVersion)
|
||||||
await updateCargoVersion(newVersion)
|
await updateCargoVersion(newVersion)
|
||||||
|
await updateCargoLockVersion(newVersion)
|
||||||
await updateTauriConfigVersion(newVersion)
|
await updateTauriConfigVersion(newVersion)
|
||||||
console.log('[SUCCESS]: All version updates completed successfully!')
|
console.log('[SUCCESS]: All version updates completed successfully!')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+41
-19
@@ -30,29 +30,51 @@ function is_valid_ip() {
|
|||||||
is_valid_ipv4 "$1" || is_valid_ipv6 "$1"
|
is_valid_ipv4 "$1" || is_valid_ipv6 "$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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
|
||||||
|
function dns_of() {
|
||||||
|
local dns line ip='^[0-9A-Fa-f]*[.:][0-9A-Fa-f.:]*(%[[:alnum:]_.-]+)?$'
|
||||||
|
dns=$(networksetup -getdnsservers "$1") || return 1
|
||||||
|
if [ "$dns" = "There aren't any DNS Servers set on $1." ]; then
|
||||||
|
echo "empty"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
[ -n "$dns" ] || return 1
|
||||||
|
while read -r line; do
|
||||||
|
[[ $line =~ $ip ]] || return 1
|
||||||
|
done <<<"$dns"
|
||||||
|
echo "$dns"
|
||||||
|
}
|
||||||
|
|
||||||
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
|
[ $# -lt 1 ] && echo "Usage: $0 <IP address>" && exit 1
|
||||||
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
|
! is_valid_ip "$1" && echo "$1 is not a valid IP address." && exit 1
|
||||||
|
|
||||||
nic=$(route -n get default | grep "interface" | awk '{print $2}')
|
# Backups are keyed by service ID, which survives renaming the service and renumbering its interface.
|
||||||
hardware_port=$(networksetup -listnetworkserviceorder | awk -v dev="$nic" '
|
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
|
||||||
/^\([0-9]+\) /{port=$0; sub(/^\([0-9]+\) /, "", port)}
|
hardware_port=$(sc_value "Setup:/Network/Service/$service_id" UserDefinedName)
|
||||||
/\(Hardware Port:/{interface=$NF;sub(/\)/, "", interface); if (interface == dev) {print port; exit}}
|
[ -z "$hardware_port" ] && echo "No primary network service." && exit 1
|
||||||
')
|
|
||||||
|
|
||||||
original_dns=$(networksetup -getdnsservers "$hardware_port")
|
original_dns=$(dns_of "$hardware_port") || { echo "Cannot read DNS servers of $hardware_port." >&2; exit 1; }
|
||||||
|
|
||||||
is_valid_dns=false
|
backup=".original_dns/$service_id"
|
||||||
for ip in $original_dns; do
|
mkdir -p .original_dns || exit 1
|
||||||
ip=$(echo "$ip" | tr -d '[:space:]')
|
# Older releases kept a single backup for whichever service was primary.
|
||||||
if [ -n "$ip" ] && (is_valid_ipv4 "$ip" || is_valid_ipv6 "$ip"); then
|
if [ -f .original_dns.txt ]; then
|
||||||
is_valid_dns=true
|
if [ -f "$backup" ]; then
|
||||||
break
|
rm -f .original_dns.txt || exit 1
|
||||||
|
else
|
||||||
|
mv .original_dns.txt "$backup" || exit 1
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$is_valid_dns" = false ]; then
|
|
||||||
echo "empty" >.original_dns.txt
|
|
||||||
else
|
|
||||||
echo "$original_dns" >.original_dns.txt
|
|
||||||
fi
|
fi
|
||||||
networksetup -setdnsservers "$hardware_port" "$1"
|
# 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"
|
||||||
|
[ "$(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
|
||||||
|
|||||||
+57
-8
@@ -1,13 +1,62 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
nic=$(route -n get default | grep "interface" | awk '{print $2}')
|
[ $# -lt 1 ] && echo "Usage: $0 <placeholder IP address>" && exit 1
|
||||||
|
|
||||||
hardware_port=$(networksetup -listnetworkserviceorder | awk -v dev="$nic" '
|
# Prints one value of a SystemConfiguration dictionary, or nothing when the key is absent.
|
||||||
/^\([0-9]+\) /{port=$0; sub(/^\([0-9]+\) /, "", port)}
|
function sc_value() {
|
||||||
/\(Hardware Port:/{interface=$NF;sub(/\)/, "", interface); if (interface == dev) {print port; exit}}
|
printf 'show %s\nquit\n' "$1" | scutil | sed -n "s/^ *$2 : //p"
|
||||||
')
|
}
|
||||||
|
|
||||||
|
# Prints the service's DNS servers, or "empty" when none is set; fails on any other answer.
|
||||||
|
function dns_of() {
|
||||||
|
local dns line ip='^[0-9A-Fa-f]*[.:][0-9A-Fa-f.:]*(%[[:alnum:]_.-]+)?$'
|
||||||
|
dns=$(networksetup -getdnsservers "$1") || return 1
|
||||||
|
if [ "$dns" = "There aren't any DNS Servers set on $1." ]; then
|
||||||
|
echo "empty"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
[ -n "$dns" ] || return 1
|
||||||
|
while read -r line; do
|
||||||
|
[[ $line =~ $ip ]] || return 1
|
||||||
|
done <<<"$dns"
|
||||||
|
echo "$dns"
|
||||||
|
}
|
||||||
|
|
||||||
|
result=0
|
||||||
|
# Older releases kept a single backup for whichever service was primary.
|
||||||
if [ -f .original_dns.txt ]; then
|
if [ -f .original_dns.txt ]; then
|
||||||
original_dns=$(cat .original_dns.txt)
|
service_id=$(sc_value State:/Network/Global/IPv4 PrimaryService)
|
||||||
networksetup -setdnsservers "$hardware_port" $original_dns
|
if [ -z "$service_id" ] || ! mkdir -p .original_dns; then
|
||||||
rm -rf .original_dns.txt
|
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
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
result=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$backup" || result=1
|
||||||
|
done
|
||||||
|
rmdir .original_dns 2>/dev/null
|
||||||
|
exit $result
|
||||||
|
|||||||
@@ -14,22 +14,13 @@ async function resolveUpdater() {
|
|||||||
const options = { owner: context.repo.owner, repo: context.repo.repo }
|
const options = { owner: context.repo.owner, repo: context.repo.repo }
|
||||||
const github = getOctokit(process.env.GITHUB_TOKEN)
|
const github = getOctokit(process.env.GITHUB_TOKEN)
|
||||||
|
|
||||||
const { data: tags } = await github.rest.repos.listTags({
|
const { data: latestRelease } =
|
||||||
...options,
|
await github.rest.repos.getLatestRelease(options)
|
||||||
per_page: 10,
|
const tag = { name: latestRelease.tag_name }
|
||||||
page: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
const tag = tags.find((t) => t.name.startsWith('v'))
|
|
||||||
|
|
||||||
console.log(tag)
|
console.log(tag)
|
||||||
console.log()
|
console.log()
|
||||||
|
|
||||||
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
|
|
||||||
...options,
|
|
||||||
tag: tag.name,
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
notes: await resolveUpdateLog(tag.name), // use Changelog.md
|
notes: await resolveUpdateLog(tag.name), // use Changelog.md
|
||||||
|
|||||||
+3
-2
@@ -40,10 +40,11 @@ async function resolveUpdater() {
|
|||||||
const tags = allTags
|
const tags = allTags
|
||||||
console.log(`Retrieved ${tags.length} tags in total`)
|
console.log(`Retrieved ${tags.length} tags in total`)
|
||||||
|
|
||||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/
|
|
||||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i
|
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i
|
||||||
|
|
||||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name))
|
const { data: latestRelease } =
|
||||||
|
await github.rest.repos.getLatestRelease(options)
|
||||||
|
const stableTag = { name: latestRelease.tag_name }
|
||||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name))
|
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name))
|
||||||
|
|
||||||
console.log('All tags:', tags.map((t) => t.name).join(', '))
|
console.log('All tags:', tags.map((t) => t.name).join(', '))
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Backend Architecture Guardrails
|
||||||
|
|
||||||
|
These rules supplement the repository's root `AGENTS.md`.
|
||||||
|
|
||||||
|
- **Configuration ownership.** `config` owns source configuration, drafts and
|
||||||
|
persistence. The effects layer owns patch orchestration. Rollbackable
|
||||||
|
transactions publish state after persistence and commit; already applied
|
||||||
|
runtime and independently committed state remain authoritative even when a
|
||||||
|
later step fails. Preserve that state, its notifications and the error.
|
||||||
|
- **Effect ownership.** The declarative registry classifies every Verge patch field,
|
||||||
|
including fields without effects and platform-specific fields. Effects
|
||||||
|
reconcile toward the requested state with explicit dependencies and ordering.
|
||||||
|
Configuration rollback does not imply reversal of OS or lifecycle effects.
|
||||||
|
- **Shared wire contract.** Notifications are typed. Event names, status values
|
||||||
|
and payload shapes stay compatible with frontend producers and consumers.
|
||||||
|
- **Synchronization ownership.** Configuration-write serialization precedes
|
||||||
|
configuration-update ownership, which precedes Core lifecycle ownership.
|
||||||
|
When both are held, the lifecycle lock precedes the DNS lock. Independent
|
||||||
|
transactions retain their ownership boundaries; asynchronous work does not
|
||||||
|
hold synchronous data guards across suspension.
|
||||||
|
- **Retry ownership.** Bounded retries share backend policy while preserving
|
||||||
|
attempts, delays and terminal errors. Background monitors own their lifetime
|
||||||
|
and cancellation separately; waiting for a condition or traversing distinct
|
||||||
|
candidates is not a retry policy.
|
||||||
+30
-26
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "clash-verge"
|
name = "clash-verge"
|
||||||
version = "2.5.4"
|
version = "2.5.9"
|
||||||
description = "clash verge"
|
description = "clash verge"
|
||||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
@@ -8,7 +8,7 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
|
|||||||
default-run = "clash-verge"
|
default-run = "clash-verge"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.95"
|
rust-version = "1.99"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
@@ -23,13 +23,15 @@ tauri-dev = ["dep:tauri-plugin-devtools"]
|
|||||||
tokio-trace = ["clash-verge-logging/tokio-trace"]
|
tokio-trace = ["clash-verge-logging/tokio-trace"]
|
||||||
chrome-trace = ["clash-verge-logging/chrome-trace"]
|
chrome-trace = ["clash-verge-logging/chrome-trace"]
|
||||||
clippy = ["tauri/test"]
|
clippy = ["tauri/test"]
|
||||||
perf-harness = ["dep:tauri-plugin-wdio-webdriver"]
|
# The perf-harness feature and its main.rs wiring are parked until
|
||||||
|
# tauri-plugin-wdio-webdriver publishes a tauri 3 release; restore both
|
||||||
|
# from git history together with the crate dependency.
|
||||||
|
|
||||||
[package.metadata.bundle]
|
[package.metadata.bundle]
|
||||||
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.6.3", features = [] }
|
tauri-build = { version = "3.0.0-alpha.3", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clash-verge-draft = { workspace = true }
|
clash-verge-draft = { workspace = true }
|
||||||
@@ -42,12 +44,12 @@ tauri-plugin-clash-verge-sysinfo = { workspace = true }
|
|||||||
tauri-plugin-clipboard-manager = { workspace = true }
|
tauri-plugin-clipboard-manager = { workspace = true }
|
||||||
tauri = { workspace = true, features = [
|
tauri = { workspace = true, features = [
|
||||||
"protocol-asset",
|
"protocol-asset",
|
||||||
"devtools",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"image-ico",
|
"image-ico",
|
||||||
"image-png",
|
"image-png",
|
||||||
] }
|
] }
|
||||||
|
tauri-runtime-wry = { workspace = true, features = ["devtools"] }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
@@ -66,7 +68,6 @@ dunce = "1.0.5"
|
|||||||
nanoid = "0.5"
|
nanoid = "0.5"
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
boa_engine = "0.22.0"
|
boa_engine = "0.22.0"
|
||||||
once_cell = { version = "1.21.4", features = ["parking_lot"] }
|
|
||||||
percent-encoding = "2.3.2"
|
percent-encoding = "2.3.2"
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
@@ -74,48 +75,51 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
|
|||||||
"guard",
|
"guard",
|
||||||
] }
|
] }
|
||||||
network-interface = { version = "2.0.5", features = ["serde"] }
|
network-interface = { version = "2.0.5", features = ["serde"] }
|
||||||
tauri-plugin-shell = "2.3.6"
|
tauri-plugin-shell = "3.0.0-alpha.2"
|
||||||
tauri-plugin-opener = "2.5.5"
|
tauri-plugin-opener = "3.0.0-alpha.2"
|
||||||
tauri-plugin-dialog = "2.7.3"
|
tauri-plugin-dialog = "3.0.0-alpha.3"
|
||||||
tauri-plugin-fs = "2.5.2"
|
tauri-plugin-fs = "3.0.0-alpha.2"
|
||||||
tauri-plugin-process = "2.3.1"
|
tauri-plugin-process = "3.0.0-alpha.2"
|
||||||
tauri-plugin-deep-link = "2.4.10"
|
tauri-plugin-deep-link = "3.0.0-alpha.2"
|
||||||
tauri-plugin-window-state = "2.4.1"
|
tauri-plugin-window-state = "3.0.0-alpha.2"
|
||||||
zip = "8.6.0"
|
zip = "9.0.1"
|
||||||
flate2 = "1.1.10"
|
flate2 = "1.1.10"
|
||||||
reqwest_dav = "0.3.3"
|
reqwest_dav = "0.3.3"
|
||||||
aes-gcm = { version = "0.11.1" }
|
aes-gcm = { version = "0.11.1" }
|
||||||
base64 = "0.23.1"
|
base64 = "0.23.1"
|
||||||
|
minisign-verify = "0.3.0"
|
||||||
sha2 = "0.11"
|
sha2 = "0.11"
|
||||||
getrandom = "0.4.3"
|
getrandom = "0.4.3"
|
||||||
futures = "0.3.34"
|
futures = "0.3.34"
|
||||||
gethostname = "1.1.0"
|
gethostname = "1.1.0"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
tauri-plugin-notification = "2.4.0"
|
tauri-plugin-notification = "3.0.0-alpha.2"
|
||||||
tokio-stream = "0.1.19"
|
tokio-stream = "0.1.19"
|
||||||
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||||
tauri-plugin-http = "2.6.0"
|
tauri-plugin-http = "3.0.0-alpha.2"
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tauri-plugin-devtools = { version = "2.2.1", optional = true }
|
tauri-plugin-devtools = { version = "3.0.0-alpha.0", 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", branch = "feat/tauri-v3" }
|
||||||
async-trait = "0.1.92"
|
async-trait = "0.1.92"
|
||||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.1", version = "2.7.1", features = [
|
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",
|
"client",
|
||||||
] }
|
] }
|
||||||
arc-swap = "1.9.2"
|
arc-swap = "1.9.2"
|
||||||
tokio-rustls = "0.26.5"
|
tokio-rustls = "0.26.6"
|
||||||
rustls = { version = "0.23.45", features = ["ring"] }
|
rustls = { version = "0.23.45", features = ["ring"] }
|
||||||
webpki-roots = "1.0.9"
|
webpki-roots = "1.0.9"
|
||||||
dark-light = "3.0.0"
|
dark-light = "3.0.0"
|
||||||
bytes = "1.12.1"
|
bytes = "1.12.1"
|
||||||
tokio-util = { version = "0.7.19", features = ["time"] }
|
tokio-util = { version = "0.7.19", features = ["time"] }
|
||||||
|
specta = { version = "=2.0.0-rc.26", features = ["derive", "function", "serde_json"] }
|
||||||
|
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
|
||||||
|
specta-typescript = "=0.0.13"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2.189"
|
libc = "0.2.190"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
|
objc2 = "0.6.5"
|
||||||
objc2 = "0.6.4"
|
|
||||||
objc2-foundation = { version = "0.3.2", features = [
|
objc2-foundation = { version = "0.3.2", features = [
|
||||||
"NSString",
|
"NSString",
|
||||||
"NSDictionary",
|
"NSDictionary",
|
||||||
@@ -146,9 +150,9 @@ windows = { version = "0.62.2", features = ["Win32_Globalization"] }
|
|||||||
windows-sys.workspace = true
|
windows-sys.workspace = true
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-autostart = "2.5.1"
|
tauri-plugin-autostart = "3.0.0-alpha.2"
|
||||||
tauri-plugin-global-shortcut = "2.3.2"
|
tauri-plugin-global-shortcut = "3.0.0-alpha.2"
|
||||||
tauri-plugin-updater = "2.11.0"
|
tauri-plugin-updater = "3.0.0-alpha.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { workspace = true }
|
criterion = { workspace = true }
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
"dialog:allow-open",
|
"dialog:allow-open",
|
||||||
"global-shortcut:allow-is-registered",
|
"global-shortcut:allow-is-registered",
|
||||||
"global-shortcut:allow-register",
|
"global-shortcut:allow-register",
|
||||||
"global-shortcut:allow-register-all",
|
|
||||||
"global-shortcut:allow-unregister",
|
"global-shortcut:allow-unregister",
|
||||||
"global-shortcut:allow-unregister-all",
|
"global-shortcut:allow-unregister-all",
|
||||||
"process:allow-exit",
|
"process:allow-exit",
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
chmod +x /usr/bin/clash-verge-service-install
|
||||||
|
chmod +x /usr/bin/clash-verge-service-uninstall
|
||||||
|
chmod +x /usr/bin/clash-verge-service
|
||||||
|
|
||||||
|
# The enforcement file exists in both Enforcing and Permissive mode.
|
||||||
|
if [ ! -e /sys/fs/selinux/enforce ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x /usr/sbin/semanage ] || [ ! -x /usr/sbin/restorecon ]; then
|
||||||
|
echo "Warning: persistent SELinux rules were not installed; install policycoreutils and policycoreutils-python-utils, then reinstall this package. The service installer repairs executable labels, but a system relabel can reset them." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep these local rules on removal: they may predate this package or serve a standalone development service.
|
||||||
|
for service in clash-verge-service clash-verge-service-dev; do
|
||||||
|
for directory in bin cores; do
|
||||||
|
target="/var/lib/$service/$directory"
|
||||||
|
if [ -L "/var/lib/$service" ] || [ -L "$target" ]; then
|
||||||
|
echo "Refusing to relabel a symlinked service directory: $target" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
spec="$target(/.*)?"
|
||||||
|
# Older semanage versions reject -a when an exact rule already exists.
|
||||||
|
if ! add_error="$(/usr/sbin/semanage fcontext -a -t bin_t "$spec" 2>&1)"; then
|
||||||
|
if ! /usr/sbin/semanage fcontext -m -t bin_t "$spec"; then
|
||||||
|
printf '%s\n' "$add_error" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -d "$target" ]; then
|
||||||
|
/usr/sbin/restorecon -R "$target"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
@@ -901,6 +901,7 @@ Section Install
|
|||||||
|
|
||||||
nsExec::Exec 'netsh int tcp res'
|
nsExec::Exec 'netsh int tcp res'
|
||||||
|
|
||||||
|
!include "Win\RestartManager.nsh"
|
||||||
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
!insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||||
!insertmacro CheckAllVergeProcesses
|
!insertmacro CheckAllVergeProcesses
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut args = std::env::args_os().skip(1);
|
||||||
|
let output = args
|
||||||
|
.next()
|
||||||
|
.map(std::path::PathBuf::from)
|
||||||
|
.unwrap_or_else(|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"));
|
||||||
|
if args.next().is_some() {
|
||||||
|
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "expected at most one output path").into());
|
||||||
|
}
|
||||||
|
app_lib::ipc::builder().export(specta_typescript::Typescript::default(), output)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{cmd::StringifyErr as _, feat, utils::dirs};
|
use crate::{
|
||||||
|
cmd::StringifyErr as _,
|
||||||
|
core::{SilentUpdater, updater::DownloadEvent},
|
||||||
|
feat,
|
||||||
|
utils::dirs,
|
||||||
|
};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use tauri::{AppHandle, Manager as _};
|
use tauri::{AppHandle, Manager as _, ipc::Channel};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn open_app_dir() -> CmdResult<()> {
|
pub async fn open_app_dir() -> CmdResult<()> {
|
||||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||||
open::that(app_dir).stringify_err()
|
open::that(app_dir).stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn open_core_dir() -> CmdResult<()> {
|
pub async fn open_core_dir() -> CmdResult<()> {
|
||||||
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
|
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
|
||||||
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
|
||||||
@@ -17,12 +24,14 @@ pub async fn open_core_dir() -> CmdResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn open_logs_dir() -> CmdResult<()> {
|
pub async fn open_logs_dir() -> CmdResult<()> {
|
||||||
let log_dir = dirs::app_logs_dir().stringify_err()?;
|
let log_dir = dirs::app_logs_dir().stringify_err()?;
|
||||||
open::that(log_dir).stringify_err()
|
open::that(log_dir).stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn open_devtools(app_handle: AppHandle) {
|
pub fn open_devtools(app_handle: AppHandle) {
|
||||||
if let Some(window) = app_handle.get_webview_window("main") {
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
if !window.is_devtools_open() {
|
if !window.is_devtools_open() {
|
||||||
@@ -34,28 +43,61 @@ pub fn open_devtools(app_handle: AppHandle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn exit_app() {
|
pub async fn exit_app() {
|
||||||
feat::quit().await;
|
feat::quit().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn restart_app() -> CmdResult<()> {
|
pub async fn restart_app() -> CmdResult<()> {
|
||||||
feat::restart_app().await;
|
feat::restart_app().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_app_dir() -> CmdResult<String> {
|
#[specta::specta]
|
||||||
|
pub async fn install_update(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
version: std::string::String,
|
||||||
|
on_event: Channel<DownloadEvent>,
|
||||||
|
) -> CmdResult<bool> {
|
||||||
|
let version: String = version.into();
|
||||||
|
SilentUpdater::global()
|
||||||
|
.install_update(&app_handle, &version, |event| {
|
||||||
|
let _ = on_event.send(event);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.stringify_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn cancel_update_download() {
|
||||||
|
SilentUpdater::global().cancel_download();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_app_dir() -> CmdResult<std::string::String> {
|
||||||
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
|
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
|
||||||
Ok(app_home_dir)
|
Ok(app_home_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
#[specta::specta]
|
||||||
feat::download_icon_cache(url, name).await
|
pub async fn download_icon_cache(
|
||||||
|
url: std::string::String,
|
||||||
|
name: std::string::String,
|
||||||
|
) -> CmdResult<std::string::String> {
|
||||||
|
let url: String = url.into();
|
||||||
|
let name: String = name.into();
|
||||||
|
feat::download_icon_cache(url, name).await.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
|
#[specta::specta]
|
||||||
feat::copy_icon_file(path, icon_info).await
|
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
|
||||||
|
let path: String = path.into();
|
||||||
|
feat::copy_icon_file(path, icon_info).await.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,33 +4,44 @@ use feat::LocalBackupFile;
|
|||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn create_local_backup() -> CmdResult<()> {
|
pub async fn create_local_backup() -> CmdResult<()> {
|
||||||
feat::create_local_backup().await.stringify_err()
|
feat::create_local_backup().await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
|
||||||
feat::list_local_backup().await.stringify_err()
|
feat::list_local_backup().await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
|
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||||
|
let filename: String = filename.into();
|
||||||
feat::delete_local_backup(filename).await.stringify_err()
|
feat::delete_local_backup(filename).await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
|
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
|
||||||
|
let filename: String = filename.into();
|
||||||
feat::restore_local_backup(filename)
|
feat::restore_local_backup(filename)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| super::proxy_aware_error(&error))
|
.map_err(|error| super::proxy_aware_error(&error))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn import_local_backup(source: String) -> CmdResult<String> {
|
#[specta::specta]
|
||||||
feat::import_local_backup(source).await.stringify_err()
|
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
|
||||||
|
let source: String = source.into();
|
||||||
|
feat::import_local_backup(source).await.map(Into::into).stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
|
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
|
||||||
|
let filename: String = filename.into();
|
||||||
|
let destination: String = destination.into();
|
||||||
feat::export_local_backup(filename, destination).await.stringify_err()
|
feat::export_local_backup(filename, destination).await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-31
@@ -1,4 +1,5 @@
|
|||||||
use super::{CmdResult, CommandFailure, WithErrorCode as _, proxy_aware_coded_error, proxy_aware_error};
|
use super::{CmdResult, CommandFailure, WithErrorCode as _, proxy_aware_coded_error, proxy_aware_error};
|
||||||
|
use crate::core::notify::NoticeStatus;
|
||||||
use crate::feat;
|
use crate::feat;
|
||||||
use crate::utils::{dirs, yaml_emitter};
|
use crate::utils::{dirs, yaml_emitter};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -11,30 +12,33 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging, logging_error};
|
||||||
use serde_yaml_ng::Mapping;
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn copy_clash_env() -> CmdResult {
|
pub async fn copy_clash_env() -> CmdResult {
|
||||||
feat::copy_clash_env().await;
|
feat::copy_clash_env().await.stringify_err()
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||||
Ok(Config::clash().await.data_arc().get_client_info())
|
Ok(Config::clash().await.data_arc().get_client_info())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
#[specta::specta]
|
||||||
feat::patch_clash(&payload)
|
pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
|
||||||
|
feat::patch_clash(&payload.0)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
|
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
|
||||||
|
let payload: String = payload.into();
|
||||||
feat::change_clash_mode(payload)
|
feat::change_clash_mode(payload)
|
||||||
.await
|
.await
|
||||||
.with_error_code("CLASH_MODE_UPDATE_FAILED")
|
.with_error_code("CLASH_MODE_UPDATE_FAILED")
|
||||||
@@ -42,12 +46,15 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
|
|||||||
|
|
||||||
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
|
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
|
#[specta::specta]
|
||||||
Ok(Config::clash().await.data_arc().get_mode().map(Into::into))
|
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
|
||||||
|
Ok(Config::clash().await.data_arc().get_mode())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
|
#[specta::specta]
|
||||||
|
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
|
||||||
|
let clash_core: String = clash_core.into();
|
||||||
logging!(info, Type::Config, "changing core to {clash_core}");
|
logging!(info, Type::Config, "changing core to {clash_core}");
|
||||||
|
|
||||||
match CoreManager::global().change_core(&clash_core).await {
|
match CoreManager::global().change_core(&clash_core).await {
|
||||||
@@ -56,14 +63,14 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFa
|
|||||||
|
|
||||||
match CoreManager::global().restart_core().await {
|
match CoreManager::global().restart_core().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
handle::Handle::notice_message("config_core::change_success", clash_core);
|
handle::Handle::notice(NoticeStatus::ConfigCoreChangeSuccess, clash_core.as_str());
|
||||||
handle::Handle::refresh_clash();
|
handle::Handle::refresh_clash();
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let failed = err.context("core changed but failed to restart");
|
let failed = err.context("core changed but failed to restart");
|
||||||
let error_msg: String = format!("{failed:#}").into();
|
let error_msg: String = format!("{failed:#}").into();
|
||||||
handle::Handle::notice_message("config_core::change_error", error_msg.clone());
|
handle::Handle::notice(NoticeStatus::ConfigCoreChangeError, error_msg.as_str());
|
||||||
logging!(error, Type::Core, "core changed but failed to restart: {error_msg}");
|
logging!(error, Type::Core, "core changed but failed to restart: {error_msg}");
|
||||||
Ok(Some(proxy_aware_coded_error(&failed, "CORE_CHANGE_FAILED")))
|
Ok(Some(proxy_aware_coded_error(&failed, "CORE_CHANGE_FAILED")))
|
||||||
}
|
}
|
||||||
@@ -72,13 +79,14 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFa
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
let error_msg: String = format!("{err:#}").into();
|
let error_msg: String = format!("{err:#}").into();
|
||||||
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
logging!(error, Type::Core, "failed to change core: {error_msg}");
|
||||||
handle::Handle::notice_message("config_core::change_error", error_msg);
|
handle::Handle::notice(NoticeStatus::ConfigCoreChangeError, error_msg.as_str());
|
||||||
Ok(Some(proxy_aware_coded_error(&err, "CORE_CHANGE_FAILED")))
|
Ok(Some(proxy_aware_coded_error(&err, "CORE_CHANGE_FAILED")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn restart_core() -> CmdResult {
|
pub async fn restart_core() -> CmdResult {
|
||||||
logging_error!(Type::Core, profiles_save_file_safe().await);
|
logging_error!(Type::Core, profiles_save_file_safe().await);
|
||||||
let result = CoreManager::global()
|
let result = CoreManager::global()
|
||||||
@@ -93,6 +101,7 @@ pub async fn restart_core() -> CmdResult {
|
|||||||
|
|
||||||
/// Replaces the managed core binary atomically instead of letting mihomo overwrite itself.
|
/// Replaces the managed core binary atomically instead of letting mihomo overwrite itself.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeReport> {
|
pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeReport> {
|
||||||
let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?;
|
let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?;
|
||||||
if report.upgraded {
|
if report.upgraded {
|
||||||
@@ -102,7 +111,9 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn test_delay(url: String) -> CmdResult<u32> {
|
#[specta::specta]
|
||||||
|
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
|
||||||
|
let url: String = url.into();
|
||||||
let result = match feat::test_delay(url).await {
|
let result = match feat::test_delay(url).await {
|
||||||
Ok(delay) => delay,
|
Ok(delay) => delay,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -114,13 +125,14 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn save_dns_config(dns_config: crate::ipc::YamlMapping) -> CmdResult {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||||
|
|
||||||
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?;
|
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config.0).stringify_err()?;
|
||||||
fs::write(&dns_path, yaml_str).await.stringify_err()?;
|
fs::write(&dns_path, yaml_str).await.stringify_err()?;
|
||||||
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
|
||||||
|
|
||||||
@@ -128,42 +140,54 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn take_dns_override_notice() -> bool {
|
pub fn take_dns_override_notice() -> bool {
|
||||||
crate::config::dns::take_dns_override_notice()
|
crate::config::dns::take_dns_override_notice()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_dns_override(enabled: bool, confirmation: Option<String>) -> CmdResult<feat::DnsOverrideOutcome> {
|
#[specta::specta]
|
||||||
feat::set_dns_override(enabled, confirmation)
|
pub async fn set_dns_override(
|
||||||
|
profile_uid: std::string::String,
|
||||||
|
enabled: bool,
|
||||||
|
confirmation: Option<std::string::String>,
|
||||||
|
) -> CmdResult<feat::DnsOverrideOutcome> {
|
||||||
|
let profile_uid: String = profile_uid.into();
|
||||||
|
let confirmation = confirmation.map(String::from);
|
||||||
|
feat::set_dns_override(profile_uid, enabled, confirmation)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
|
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
||||||
if apply {
|
if apply {
|
||||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||||
|
|
||||||
if !dns_path.exists() {
|
if !dns_path.exists() {
|
||||||
logging!(warn, Type::Config, "DNS config file not found");
|
logging!(warn, Type::Config, "DNS config file not found: {}", dns_path.display());
|
||||||
return Err("DNS config file not found".into());
|
return Err("DNS config file not found".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
|
let dns_yaml = fs::read_to_string(&dns_path).await.stringify_err_log(|e| {
|
||||||
logging!(error, Type::Config, "Failed to read DNS config: {e}");
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
"Failed to read DNS config {}: {e}",
|
||||||
|
dns_path.display()
|
||||||
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let patch_config = serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
|
serde_yaml_ng::from_str::<serde_yaml_ng::Mapping>(&dns_yaml).stringify_err_log(|e| {
|
||||||
logging!(error, Type::Config, "Failed to parse DNS config: {e}");
|
logging!(
|
||||||
|
error,
|
||||||
|
Type::Config,
|
||||||
|
"Failed to parse DNS config {}: {e}",
|
||||||
|
dns_path.display()
|
||||||
|
);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut patch = serde_yaml_ng::Mapping::new();
|
|
||||||
patch.insert("dns".into(), patch_config.into());
|
|
||||||
|
|
||||||
Config::runtime().await.edit_draft(|d| {
|
|
||||||
d.patch_config(&patch);
|
|
||||||
});
|
|
||||||
|
|
||||||
CoreManager::global().update_config_checked().await.map_err(|error| {
|
CoreManager::global().update_config_checked().await.map_err(|error| {
|
||||||
logging!(error, Type::Config, "failed to apply the config with DNS: {error:#}");
|
logging!(error, Type::Config, "failed to apply the config with DNS: {error:#}");
|
||||||
proxy_aware_error(&error)
|
proxy_aware_error(&error)
|
||||||
@@ -187,20 +211,22 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
|
#[specta::specta]
|
||||||
|
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
|
||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
|
||||||
|
|
||||||
match fs::read_to_string(&dns_path).await {
|
match fs::read_to_string(&dns_path).await {
|
||||||
Ok(content) => Ok(Some(content.into())),
|
Ok(content) => Ok(Some(content)),
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
Err(error) => Err(CommandFailure::plain(error)),
|
Err(error) => Err(CommandFailure::plain(error)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
||||||
let app_dir = dirs::app_home_dir().stringify_err()?;
|
let app_dir = dirs::app_home_dir().stringify_err()?;
|
||||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
||||||
@@ -210,12 +236,14 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
|
|||||||
return Ok(ValidationOutcome::invalid_from_message("DNS config file not found"));
|
return Ok(ValidationOutcome::invalid_from_message("DNS config file not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreConfigValidator::validate_config_file_outcome(dns_path_str, None)
|
// A fragment, not a runnable config; the merged result is validated on apply.
|
||||||
|
CoreConfigValidator::validate_config_file_outcome(dns_path_str, Some(true))
|
||||||
.await
|
.await
|
||||||
.stringify_err()
|
.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
|
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
|
||||||
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
|
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
|
||||||
Ok(logs)
|
Ok(logs)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::module::lightweight;
|
use crate::core::lightweight;
|
||||||
|
|
||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn entry_lightweight_mode() -> CmdResult {
|
pub async fn entry_lightweight_mode() -> CmdResult {
|
||||||
lightweight::entry_lightweight_mode().await;
|
lightweight::entry_lightweight_mode().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn probe_listener(request: ListenerProbe) -> CmdResult<ListenerProbeOutcome> {
|
pub async fn probe_listener(request: ListenerProbe) -> CmdResult<ListenerProbeOutcome> {
|
||||||
feat::probe_listener(request).await.stringify_err()
|
feat::probe_listener(request).await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult<SaveProxyPortsOutcome> {
|
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult<SaveProxyPortsOutcome> {
|
||||||
feat::save_proxy_ports(settings)
|
feat::save_proxy_ports(settings)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::time::Duration;
|
|||||||
use tauri::{command, ipc::Channel};
|
use tauri::{command, ipc::Channel};
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn get_unlock_items() -> Vec<UnlockItem> {
|
pub fn get_unlock_items() -> Vec<UnlockItem> {
|
||||||
clash_verge_media_unlock::default_unlock_items()
|
clash_verge_media_unlock::default_unlock_items()
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ async fn create_client() -> Result<Client, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<UnlockItem>, String> {
|
pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<UnlockItem>, String> {
|
||||||
let client = create_client().await?;
|
let client = create_client().await?;
|
||||||
Ok(clash_verge_media_unlock::check_media_unlock(&client, |item| {
|
Ok(clash_verge_media_unlock::check_media_unlock(&client, |item| {
|
||||||
@@ -35,6 +37,7 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
|
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
|
||||||
let client = create_client().await?;
|
let client = create_client().await?;
|
||||||
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
|
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use smartstring::alias::String;
|
|||||||
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
|
||||||
|
|
||||||
/// Structured command failure returned to the frontend.
|
/// Structured command failure returned to the frontend.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||||
pub struct CommandFailure {
|
pub struct CommandFailure {
|
||||||
/// Stable frontend identifier, when classified.
|
/// Stable frontend identifier, when classified.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ use crate::cmd::StringifyErr as _;
|
|||||||
use crate::core::{proxy_control, sysopt::Sysopt};
|
use crate::core::{proxy_control, sysopt::Sysopt};
|
||||||
|
|
||||||
use gethostname::gethostname;
|
use gethostname::gethostname;
|
||||||
use network_interface::NetworkInterface;
|
|
||||||
use serde_yaml_ng::Mapping;
|
|
||||||
use sysproxy::{Autoproxy, Sysproxy};
|
use sysproxy::{Autoproxy, Sysproxy};
|
||||||
use tauri_plugin_clash_verge_sysinfo;
|
use tauri_plugin_clash_verge_sysinfo;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
#[specta::specta]
|
||||||
|
pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
|
||||||
Sysopt::global().wait_idle().await;
|
Sysopt::global().wait_idle().await;
|
||||||
// With no network service there is no proxy configured anywhere, which reads as disabled.
|
// With no network service there is no proxy configured anywhere, which reads as disabled.
|
||||||
let sys_proxy = match Sysproxy::get_system_proxy() {
|
let sys_proxy = match Sysproxy::get_system_proxy() {
|
||||||
@@ -17,40 +16,40 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
|
|||||||
other => other.stringify_err()?,
|
other => other.stringify_err()?,
|
||||||
};
|
};
|
||||||
let Sysproxy {
|
let Sysproxy {
|
||||||
ref host,
|
host,
|
||||||
ref bypass,
|
bypass,
|
||||||
ref port,
|
port,
|
||||||
ref enable,
|
enable,
|
||||||
} = sys_proxy;
|
} = sys_proxy;
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
Ok(SystemProxy {
|
||||||
map.insert("enable".into(), (*enable).into());
|
enable,
|
||||||
map.insert("server".into(), format!("{}:{}", host, port).into());
|
server: format!("{host}:{port}"),
|
||||||
map.insert("bypass".into(), bypass.as_str().into());
|
bypass,
|
||||||
Ok(map)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
|
#[specta::specta]
|
||||||
|
pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
|
||||||
Sysopt::global().wait_idle().await;
|
Sysopt::global().wait_idle().await;
|
||||||
let auto_proxy = match Autoproxy::get_auto_proxy() {
|
let auto_proxy = match Autoproxy::get_auto_proxy() {
|
||||||
Err(error) if proxy_control::is_missing_network_service(&error) => Autoproxy::default(),
|
Err(error) if proxy_control::is_missing_network_service(&error) => Autoproxy::default(),
|
||||||
other => other.stringify_err()?,
|
other => other.stringify_err()?,
|
||||||
};
|
};
|
||||||
let Autoproxy { ref enable, ref url } = auto_proxy;
|
let Autoproxy { enable, url } = auto_proxy;
|
||||||
|
|
||||||
let mut map = Mapping::new();
|
Ok(AutoProxy { enable, url })
|
||||||
map.insert("enable".into(), (*enable).into());
|
|
||||||
map.insert("url".into(), url.as_str().into());
|
|
||||||
Ok(map)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn get_embedded_server_port() -> CmdResult<u16> {
|
pub fn get_embedded_server_port() -> CmdResult<u16> {
|
||||||
crate::utils::server::embedded_server_port().stringify_err()
|
crate::utils::server::embedded_server_port().stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn get_system_hostname() -> String {
|
pub fn get_system_hostname() -> String {
|
||||||
match gethostname().into_string() {
|
match gethostname().into_string() {
|
||||||
Ok(name) => name,
|
Ok(name) => name,
|
||||||
@@ -62,12 +61,14 @@ pub fn get_system_hostname() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn get_network_interfaces() -> Vec<String> {
|
pub fn get_network_interfaces() -> Vec<String> {
|
||||||
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
|
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
#[specta::specta]
|
||||||
|
pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfaceView>> {
|
||||||
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
|
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
|
||||||
|
|
||||||
let names = get_network_interfaces();
|
let names = get_network_interfaces();
|
||||||
@@ -77,9 +78,22 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
|
|||||||
|
|
||||||
for interface in interfaces {
|
for interface in interfaces {
|
||||||
if names.contains(&interface.name) {
|
if names.contains(&interface.name) {
|
||||||
result.push(interface);
|
result.push(interface.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, specta::Type)]
|
||||||
|
pub struct SystemProxy {
|
||||||
|
pub enable: bool,
|
||||||
|
pub server: String,
|
||||||
|
pub bypass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, specta::Type)]
|
||||||
|
pub struct AutoProxy {
|
||||||
|
pub enable: bool,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|||||||
+116
-18
@@ -1,6 +1,7 @@
|
|||||||
use super::{CmdResult, StringifyErr as _, WithErrorCode as _, coded_error};
|
use super::{CmdResult, StringifyErr as _, WithErrorCode as _, coded_error};
|
||||||
use crate::cmd::validate::{ValidationNoticeTarget, handle_validation_notice};
|
use crate::cmd::validate::{ValidationNoticeTarget, handle_validation_notice};
|
||||||
use crate::config::profiles;
|
use crate::config::profiles;
|
||||||
|
use crate::core::notify::NoticeStatus;
|
||||||
use crate::utils::window_manager::WindowManager;
|
use crate::utils::window_manager::WindowManager;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{
|
config::{
|
||||||
@@ -32,6 +33,7 @@ fn profile_import_error(err: &anyhow::Error) -> std::string::String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
|
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
|
||||||
let draft = Config::profiles().await;
|
let draft = Config::profiles().await;
|
||||||
let data = draft.data_arc();
|
let data = draft.data_arc();
|
||||||
@@ -39,6 +41,7 @@ pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
||||||
match feat::enhance_profiles().await {
|
match feat::enhance_profiles().await {
|
||||||
Ok(outcome) if outcome.is_valid() => {
|
Ok(outcome) if outcome.is_valid() => {
|
||||||
@@ -64,6 +67,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
|
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||||
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
|
||||||
Ok(it) => it,
|
Ok(it) => it,
|
||||||
@@ -93,7 +97,10 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
|
||||||
|
let active_id: String = active_id.into();
|
||||||
|
let over_id: String = over_id.into();
|
||||||
match profiles_reorder_safe(&active_id, &over_id).await {
|
match profiles_reorder_safe(&active_id, &over_id).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
|
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
|
||||||
@@ -113,16 +120,16 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn create_profile(item: ProfileCreate, file_data: Option<std::string::String>) -> CmdResult {
|
||||||
|
let item: PrfItem = item.into();
|
||||||
|
let file_data = file_data.map(String::from);
|
||||||
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
match profiles_append_item_with_filedata_safe(&item, file_data).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
profiles_save_file_safe()
|
profiles_save_file_safe()
|
||||||
.await
|
.await
|
||||||
.with_error_code("PROFILE_CREATE_FAILED")?;
|
.with_error_code("PROFILE_CREATE_FAILED")?;
|
||||||
logging_error!(Type::Timer, Timer::global().refresh().await);
|
logging_error!(Type::Timer, Timer::global().refresh().await);
|
||||||
if let Some(uid) = &item.uid {
|
|
||||||
handle::Handle::notify_profile_changed(uid);
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => Err(coded_error("PROFILE_CREATE_FAILED", err)),
|
Err(err) => Err(coded_error("PROFILE_CREATE_FAILED", err)),
|
||||||
@@ -130,7 +137,9 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
|
||||||
|
let index: String = index.into();
|
||||||
match feat::update_profile(&index, option.as_ref(), true).await {
|
match feat::update_profile(&index, option.as_ref(), true).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -141,7 +150,9 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_profile(index: String) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn delete_profile(index: std::string::String) -> CmdResult {
|
||||||
|
let index: String = index.into();
|
||||||
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||||
|
|
||||||
let profiles = Config::profiles().await;
|
let profiles = Config::profiles().await;
|
||||||
@@ -282,7 +293,7 @@ async fn handle_update_error<E: std::fmt::Display>(
|
|||||||
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
|
logging!(warn, Type::Cmd, "更新过程发生错误: {}", e,);
|
||||||
discard_and_restore(current_profile).await?;
|
discard_and_restore(current_profile).await?;
|
||||||
let message: String = e.to_string().into();
|
let message: String = e.to_string().into();
|
||||||
handle::Handle::notice_message("config_validate::boot_error", message.clone());
|
handle::Handle::notice(NoticeStatus::ConfigValidateBootError, message.as_str());
|
||||||
Ok(ValidationOutcome::invalid_from_message(message))
|
Ok(ValidationOutcome::invalid_from_message(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,9 +323,85 @@ async fn perform_config_update(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ProfileSelection {
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub current: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ProfileCreate {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub itype: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub desc: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub option: Option<PrfOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProfileCreate> for PrfItem {
|
||||||
|
fn from(item: ProfileCreate) -> Self {
|
||||||
|
Self {
|
||||||
|
itype: item.itype,
|
||||||
|
name: item.name,
|
||||||
|
desc: item.desc,
|
||||||
|
url: item.url,
|
||||||
|
option: item.option,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Deserialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ProfilePatch {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub itype: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub desc: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub file: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub selected: Option<Vec<crate::config::PrfSelected>>,
|
||||||
|
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||||
|
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||||
|
pub extra: Option<crate::config::PrfExtra>,
|
||||||
|
pub updated: Option<usize>,
|
||||||
|
pub option: Option<PrfOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProfilePatch> for PrfItem {
|
||||||
|
fn from(patch: ProfilePatch) -> Self {
|
||||||
|
Self {
|
||||||
|
itype: patch.itype,
|
||||||
|
name: patch.name,
|
||||||
|
desc: patch.desc,
|
||||||
|
file: patch.file,
|
||||||
|
url: patch.url,
|
||||||
|
selected: patch.selected,
|
||||||
|
latency_test_urls: patch.latency_test_urls,
|
||||||
|
extra: patch.extra,
|
||||||
|
updated: patch.updated,
|
||||||
|
option: patch.option,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))]
|
#[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))]
|
||||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationOutcome> {
|
#[specta::specta]
|
||||||
|
pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<ValidationOutcome> {
|
||||||
if CURRENT_SWITCHING_PROFILE
|
if CURRENT_SWITCHING_PROFILE
|
||||||
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
||||||
.is_err()
|
.is_err()
|
||||||
@@ -324,11 +411,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
|
|||||||
}
|
}
|
||||||
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
|
||||||
|
|
||||||
|
let profiles = IProfiles {
|
||||||
|
current: profiles.current,
|
||||||
|
items: None,
|
||||||
|
};
|
||||||
let target_profile = profiles.current.as_ref();
|
let target_profile = profiles.current.as_ref();
|
||||||
|
|
||||||
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
let previous_profile = Config::profiles().await.data_arc().current.clone();
|
||||||
|
|
||||||
Config::profiles().await.edit_draft(|d| d.patch_config(&profiles));
|
Config::profiles().await.edit_draft(|d| {
|
||||||
|
d.patch_config(&profiles);
|
||||||
|
});
|
||||||
|
|
||||||
perform_config_update(target_profile, previous_profile.as_ref())
|
perform_config_update(target_profile, previous_profile.as_ref())
|
||||||
.await
|
.await
|
||||||
@@ -336,15 +429,17 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<ValidationOutcome> {
|
pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> CmdResult<ValidationOutcome> {
|
||||||
let profiles = IProfiles {
|
let profiles = ProfileSelection {
|
||||||
current: Some(profile_index),
|
current: Some(profile_index),
|
||||||
items: None,
|
|
||||||
};
|
};
|
||||||
patch_profiles_config(profiles).await
|
patch_profiles_config(profiles).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn patch_profile(index: std::string::String, profile: ProfilePatch) -> CmdResult {
|
||||||
|
let profile: PrfItem = profile.into();
|
||||||
|
let index: String = index.into();
|
||||||
let profiles = Config::profiles().await;
|
let profiles = Config::profiles().await;
|
||||||
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
|
||||||
&& let Some(new_option) = profile.option.as_ref()
|
&& let Some(new_option) = profile.option.as_ref()
|
||||||
@@ -384,7 +479,8 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn view_profile(index: String) -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn view_profile(index: std::string::String) -> CmdResult {
|
||||||
let profiles = Config::profiles().await;
|
let profiles = Config::profiles().await;
|
||||||
let profiles_ref = profiles.latest_arc();
|
let profiles_ref = profiles.latest_arc();
|
||||||
let file = profiles_ref
|
let file = profiles_ref
|
||||||
@@ -408,7 +504,8 @@ pub async fn view_profile(index: String) -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
#[specta::specta]
|
||||||
|
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
|
||||||
let item = {
|
let item = {
|
||||||
let profiles = Config::profiles().await;
|
let profiles = Config::profiles().await;
|
||||||
let profiles_ref = profiles.latest_arc();
|
let profiles_ref = profiles.latest_arc();
|
||||||
@@ -428,7 +525,7 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
|||||||
.join(file.as_str());
|
.join(file.as_str());
|
||||||
match tokio::fs::try_exists(&path).await {
|
match tokio::fs::try_exists(&path).await {
|
||||||
Ok(true) => {}
|
Ok(true) => {}
|
||||||
Ok(false) => return Ok(String::new()),
|
Ok(false) => return Ok(std::string::String::new()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(coded_error(
|
return Err(coded_error(
|
||||||
"PROFILE_READ_FAILED",
|
"PROFILE_READ_FAILED",
|
||||||
@@ -439,11 +536,12 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
|
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
|
||||||
Ok(data)
|
Ok(data.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
|
#[specta::specta]
|
||||||
|
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
|
||||||
let timer = Timer::global();
|
let timer = Timer::global();
|
||||||
let next_time = timer.get_next_update_time(&uid).await;
|
let next_time = timer.get_next_update_time(&uid).await;
|
||||||
Ok(next_time)
|
Ok(next_time)
|
||||||
|
|||||||
+76
-32
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
config::Config,
|
config::Config,
|
||||||
core::{
|
core::{
|
||||||
handle::Handle,
|
handle::Handle,
|
||||||
proxy_view::{ProxyViewBuilder, ProxyViewInput, ProxyViewV1},
|
proxy_view::{GroupScope, ProxyViewBuilder, ProxyViewInput, ProxyViewV1},
|
||||||
tray::Tray,
|
tray::Tray,
|
||||||
},
|
},
|
||||||
process::AsyncHandler,
|
process::AsyncHandler,
|
||||||
@@ -12,12 +12,13 @@ use crate::{
|
|||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use serde_yaml_ng::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::{BTreeMap, HashSet},
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
sync::atomic::{AtomicBool, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
|
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn record_selected_node(group_name: String, node: String) -> CmdResult<()> {
|
pub async fn record_selected_node(group_name: String, node: String) -> CmdResult<()> {
|
||||||
crate::config::profiles::record_selected_node(&group_name, &node)
|
crate::config::profiles::record_selected_node(&group_name, &node)
|
||||||
.await
|
.await
|
||||||
@@ -25,6 +26,7 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
|
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
|
||||||
crate::config::profiles::forget_selected_node(&group_name)
|
crate::config::profiles::forget_selected_node(&group_name)
|
||||||
.await
|
.await
|
||||||
@@ -50,24 +52,93 @@ fn runtime_group_order(config: Option<&Mapping>) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mirrors Mihomo: `include-all`/`include-all-providers` replace `use` with every provider, sorted.
|
||||||
|
fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope> {
|
||||||
|
let Some(config) = config else {
|
||||||
|
return BTreeMap::new();
|
||||||
|
};
|
||||||
|
let mut all_providers = config
|
||||||
|
.get("proxy-providers")
|
||||||
|
.and_then(|providers| providers.as_mapping())
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|providers| providers.keys())
|
||||||
|
.filter_map(|name| name.as_str())
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
all_providers.sort();
|
||||||
|
let flag = |group: &Mapping, key: &str| group.get(key).and_then(|value| value.as_bool()) == Some(true);
|
||||||
|
|
||||||
|
config
|
||||||
|
.get("proxy-groups")
|
||||||
|
.and_then(|groups| groups.as_sequence())
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|group| group.as_mapping())
|
||||||
|
.filter_map(|group| {
|
||||||
|
let name = group.get("name")?.as_str()?.to_owned();
|
||||||
|
let providers = if flag(group, "include-all") || flag(group, "include-all-providers") {
|
||||||
|
all_providers.clone()
|
||||||
|
} else {
|
||||||
|
group
|
||||||
|
.get("use")
|
||||||
|
.and_then(|providers| providers.as_sequence())
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|provider| provider.as_str())
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let exclude_types = group
|
||||||
|
.get("exclude-type")
|
||||||
|
.and_then(|types| types.as_str())
|
||||||
|
.filter(|types| !types.is_empty())
|
||||||
|
.map(|types| types.split('|').map(str::to_owned).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
Some((
|
||||||
|
name,
|
||||||
|
GroupScope {
|
||||||
|
providers,
|
||||||
|
exclude_types,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
|
||||||
|
proxy_view(None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn proxy_view(provider_timeout: Option<std::time::Duration>) -> CmdResult<ProxyViewV1> {
|
||||||
let runtime = Config::runtime().await;
|
let runtime = Config::runtime().await;
|
||||||
let latest_runtime = runtime.latest_arc();
|
let latest_runtime = runtime.latest_arc();
|
||||||
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
let runtime_group_order = runtime_group_order(latest_runtime.config.as_ref());
|
||||||
|
let group_scopes = runtime_group_scopes(latest_runtime.config.as_ref());
|
||||||
|
|
||||||
let mihomo = Handle::mihomo();
|
let mihomo = Handle::mihomo();
|
||||||
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), mihomo.get_proxy_providers(),);
|
let (proxies, providers) = tokio::join!(mihomo.get_proxies(), async {
|
||||||
|
match provider_timeout {
|
||||||
|
Some(timeout) => tokio::time::timeout(timeout, mihomo.get_proxy_providers())
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(Result::ok),
|
||||||
|
None => mihomo.get_proxy_providers().await.ok(),
|
||||||
|
}
|
||||||
|
});
|
||||||
let proxies = proxies.stringify_err()?;
|
let proxies = proxies.stringify_err()?;
|
||||||
|
|
||||||
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
Ok(ProxyViewBuilder::build(ProxyViewInput {
|
||||||
runtime_group_order,
|
runtime_group_order,
|
||||||
|
group_scopes,
|
||||||
proxies,
|
proxies,
|
||||||
providers: providers.ok(),
|
providers,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
||||||
if TRAY_SYNC_RUNNING
|
if TRAY_SYNC_RUNNING
|
||||||
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
|
||||||
@@ -84,6 +155,7 @@ pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run_tray_sync_loop() {
|
async fn run_tray_sync_loop() {
|
||||||
|
// Permanent watcher: keeps syncing until a pass finds no pending sync request.
|
||||||
loop {
|
loop {
|
||||||
match Tray::global().update_menu().await {
|
match Tray::global().update_menu().await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
@@ -109,31 +181,3 @@ async fn run_tray_sync_loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(clippy::expect_used)]
|
|
||||||
mod tests {
|
|
||||||
use serde_yaml_ng::Value;
|
|
||||||
|
|
||||||
use super::runtime_group_order;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn runtime_order_keeps_first_non_empty_non_global_name() {
|
|
||||||
let config: Value = serde_yaml_ng::from_str(
|
|
||||||
r#"
|
|
||||||
proxy-groups:
|
|
||||||
- name: Beta
|
|
||||||
- name: ""
|
|
||||||
- name: GLOBAL
|
|
||||||
- name: " Alpha "
|
|
||||||
- name: Beta
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.expect("parse runtime");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
runtime_group_order(config.as_mapping()),
|
|
||||||
["Beta".to_owned(), " Alpha ".to_owned()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,39 +1,57 @@
|
|||||||
use super::CmdResult;
|
use super::CmdResult;
|
||||||
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
|
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
|
||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use clash_verge_logging::{Type, logging};
|
|
||||||
use serde_yaml_ng::Mapping;
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
#[specta::specta]
|
||||||
Ok(Config::runtime().await.latest_arc().config.clone())
|
pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>> {
|
||||||
|
Ok(Config::runtime()
|
||||||
|
.await
|
||||||
|
.latest_arc()
|
||||||
|
.config
|
||||||
|
.clone()
|
||||||
|
.map(crate::ipc::YamlMapping))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_runtime_yaml() -> CmdResult<String> {
|
#[specta::specta]
|
||||||
|
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
|
||||||
let runtime = Config::runtime().await;
|
let runtime = Config::runtime().await;
|
||||||
let runtime = runtime.latest_arc();
|
let runtime = runtime.latest_arc();
|
||||||
|
|
||||||
let config = runtime.config.as_ref();
|
let config = runtime.config.as_ref();
|
||||||
config
|
config
|
||||||
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
|
||||||
.and_then(|config| {
|
.and_then(|config| yaml_emitter::to_mihomo_config_string(config).context("failed to convert config to yaml"))
|
||||||
yaml_emitter::to_mihomo_config_string(config)
|
|
||||||
.context("failed to convert config to yaml")
|
|
||||||
.map(|s| s.into())
|
|
||||||
})
|
|
||||||
.stringify_err()
|
.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
#[specta::specta]
|
||||||
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
|
pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
|
||||||
|
Ok(RuntimeLogs(Config::runtime().await.latest_arc().chain_logs.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, specta::Type)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct RuntimeLogs(
|
||||||
|
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
|
||||||
|
HashMap<String, Vec<(String, String)>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
|
||||||
|
crate::enhance::take_discarded_keys_notice().map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
|
#[specta::specta]
|
||||||
|
pub async fn get_runtime_proxy_chain_config(
|
||||||
|
proxy_chain_exit_node: std::string::String,
|
||||||
|
) -> CmdResult<std::string::String> {
|
||||||
let runtime = Config::runtime().await;
|
let runtime = Config::runtime().await;
|
||||||
let runtime = runtime.latest_arc();
|
let runtime = runtime.latest_arc();
|
||||||
|
|
||||||
@@ -54,7 +72,7 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
proxies_chain.push(proxy.to_owned());
|
proxies_chain.push(proxy);
|
||||||
proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str());
|
proxy_name = proxy.get("dialer-proxy").map(|x| x.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,18 +81,17 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
|||||||
.find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name)
|
.find(|proxy| proxy.get("name").map(|x| x.as_str()) == proxy_name)
|
||||||
&& !proxies_chain.is_empty()
|
&& !proxies_chain.is_empty()
|
||||||
{
|
{
|
||||||
proxies_chain.push(entry_proxy.to_owned());
|
proxies_chain.push(entry_proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies_chain.reverse();
|
proxies_chain.reverse();
|
||||||
|
|
||||||
let mut config: HashMap<String, Vec<serde_yaml_ng::Value>> = HashMap::new();
|
let mut config: HashMap<String, Vec<&serde_yaml_ng::Value>> = HashMap::new();
|
||||||
|
|
||||||
config.insert("proxies".into(), proxies_chain);
|
config.insert("proxies".into(), proxies_chain);
|
||||||
|
|
||||||
yaml_emitter::to_mihomo_config_string(&config)
|
yaml_emitter::to_mihomo_config_string(&config)
|
||||||
.context("YAML generation failed")
|
.context("YAML generation failed")
|
||||||
.map(|s| s.into())
|
|
||||||
.stringify_err()
|
.stringify_err()
|
||||||
} else {
|
} else {
|
||||||
Err("failed to get proxies or proxy-groups".into())
|
Err("failed to get proxies or proxy-groups".into())
|
||||||
@@ -82,20 +99,13 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<serde_yaml_ng::Value>) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
match CoreManager::global()
|
pub async fn update_proxy_chain_config_in_runtime(
|
||||||
|
proxy_chain_config: Option<crate::ipc::YamlValue>,
|
||||||
|
) -> CmdResult<crate::core::validate::ValidationOutcome> {
|
||||||
|
let proxy_chain_config = proxy_chain_config.map(|value| value.0);
|
||||||
|
CoreManager::global()
|
||||||
.update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config))
|
.update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config))
|
||||||
.await
|
.await
|
||||||
{
|
.stringify_err()
|
||||||
Ok(outcome) if outcome.is_valid() => {}
|
|
||||||
Ok(outcome) => logging!(
|
|
||||||
warn,
|
|
||||||
Type::Core,
|
|
||||||
"Failed to apply runtime proxy chain config: {}",
|
|
||||||
outcome
|
|
||||||
),
|
|
||||||
Err(err) => logging!(error, Type::Core, "Failed to apply runtime proxy chain config: {err:#}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ use crate::{
|
|||||||
cmd::StringifyErr as _,
|
cmd::StringifyErr as _,
|
||||||
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
|
cmd::validate::{ValidationNoticeTarget, handle_validation_notice},
|
||||||
config::{Config, IProfiles, PrfItem},
|
config::{Config, IProfiles, PrfItem},
|
||||||
|
core::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
||||||
core::{
|
core::{
|
||||||
CoreManager, handle,
|
CoreManager, handle,
|
||||||
validate::{CoreConfigValidator, ValidationOutcome},
|
validate::{CoreConfigValidator, ValidationOutcome},
|
||||||
},
|
},
|
||||||
module::auto_backup::{AutoBackupManager, AutoBackupTrigger},
|
|
||||||
utils::dirs,
|
utils::dirs,
|
||||||
};
|
};
|
||||||
use clash_verge_logging::{Type, logging, logging_error};
|
use clash_verge_logging::{Type, logging, logging_error};
|
||||||
@@ -16,7 +16,13 @@ use tokio::fs;
|
|||||||
|
|
||||||
/// 保存profiles的配置
|
/// 保存profiles的配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
|
#[specta::specta]
|
||||||
|
pub async fn save_profile_file(
|
||||||
|
index: std::string::String,
|
||||||
|
file_data: Option<std::string::String>,
|
||||||
|
) -> CmdResult<ValidationOutcome> {
|
||||||
|
let index: String = index.into();
|
||||||
|
let file_data = file_data.map(String::from);
|
||||||
let file_data = match file_data {
|
let file_data = match file_data {
|
||||||
Some(d) => d,
|
Some(d) => d,
|
||||||
None => return Ok(ValidationOutcome::Valid),
|
None => return Ok(ValidationOutcome::Valid),
|
||||||
|
|||||||
+178
-18
@@ -8,30 +8,87 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn execute_service_operation_sync(status: ServiceStatus, error_code: &str) -> CmdResult {
|
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
|
||||||
|
#[serde(tag = "status", rename_all = "camelCase")]
|
||||||
|
pub enum ServiceInstallOutcome {
|
||||||
|
Installed,
|
||||||
|
#[cfg_attr(not(any(windows, test)), allow(dead_code))]
|
||||||
|
Sidecar {
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_service_operation_sync(status: ServiceStatus, error_code: &str) -> CmdResult<ServiceInstallOutcome> {
|
||||||
let manager = CoreManager::global();
|
let manager = CoreManager::global();
|
||||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
let result = {
|
||||||
if matches!(
|
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||||
&status,
|
if matches!(
|
||||||
ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired
|
&status,
|
||||||
) {
|
ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired
|
||||||
manager
|
) {
|
||||||
.controlled_stop_core_inner()
|
manager
|
||||||
.await
|
.controlled_stop_core_inner()
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, error_code))?;
|
.await
|
||||||
}
|
.map_err(|error| proxy_aware_coded_error(&error, error_code))?;
|
||||||
SERVICE_MANAGER
|
}
|
||||||
.handle_service_status(status)
|
SERVICE_MANAGER.handle_service_status(status).await
|
||||||
.await
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
return finish_service_installation(result, error_code, |reason| async move {
|
||||||
|
manager.continue_with_sidecar(Some(&reason)).await
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
result
|
||||||
|
.map(|()| ServiceInstallOutcome::Installed)
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, error_code))
|
.map_err(|error| proxy_aware_coded_error(&error, error_code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(windows, test))]
|
||||||
|
async fn finish_service_installation<Fallback, FallbackFuture>(
|
||||||
|
installation: anyhow::Result<()>,
|
||||||
|
error_code: &str,
|
||||||
|
fallback: Fallback,
|
||||||
|
) -> CmdResult<ServiceInstallOutcome>
|
||||||
|
where
|
||||||
|
Fallback: FnOnce(String) -> FallbackFuture,
|
||||||
|
FallbackFuture: std::future::Future<Output = anyhow::Result<()>>,
|
||||||
|
{
|
||||||
|
let Err(error) = installation else {
|
||||||
|
return Ok(ServiceInstallOutcome::Installed);
|
||||||
|
};
|
||||||
|
let Some(failure) = error.downcast_ref::<clash_verge_service_ipc::management::InstallationVerificationError>()
|
||||||
|
else {
|
||||||
|
return Err(proxy_aware_coded_error(&error, error_code));
|
||||||
|
};
|
||||||
|
let reasons = failure
|
||||||
|
.status
|
||||||
|
.cores
|
||||||
|
.iter()
|
||||||
|
.filter_map(|core| match &core.availability {
|
||||||
|
clash_verge_service_ipc::CoreAvailability::Rejected { reason } => Some(format!("{}: {reason}", core.name)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if reasons.is_empty() {
|
||||||
|
return Err(proxy_aware_coded_error(&error, error_code));
|
||||||
|
}
|
||||||
|
let reason = reasons.join("\n");
|
||||||
|
fallback(reason.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))?;
|
||||||
|
Ok(ServiceInstallOutcome::Sidecar { reason })
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_service() -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn install_service() -> CmdResult<ServiceInstallOutcome> {
|
||||||
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
|
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn uninstall_service() -> CmdResult {
|
pub async fn uninstall_service() -> CmdResult {
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.uninstall_service_and_start_sidecar()
|
.uninstall_service_and_start_sidecar()
|
||||||
@@ -40,26 +97,129 @@ pub async fn uninstall_service() -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reinstall_service() -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn reinstall_service() -> CmdResult<ServiceInstallOutcome> {
|
||||||
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
|
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn repair_service() -> CmdResult {
|
#[specta::specta]
|
||||||
|
pub async fn repair_service() -> CmdResult<ServiceInstallOutcome> {
|
||||||
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
|
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn continue_with_sidecar() -> CmdResult {
|
pub async fn continue_with_sidecar() -> CmdResult {
|
||||||
crate::core::CoreManager::global()
|
crate::core::CoreManager::global()
|
||||||
.continue_with_sidecar()
|
.continue_with_sidecar(None)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))
|
.map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_sidecar_failure() -> crate::core::notification::SidecarFailureSnapshot {
|
||||||
|
crate::core::notification::sidecar_failure_snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn take_service_fallback_notice() -> Option<crate::core::service::ServiceFallbackNotice> {
|
||||||
|
crate::core::service::take_service_fallback_notice()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_core_startup_error() -> Option<crate::core::manager::CoreFailure> {
|
||||||
|
crate::core::CoreManager::global().get_startup_error()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn take_service_repair_notice() -> bool {
|
||||||
|
crate::core::service::take_service_repair_notice()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn take_service_owner_notice() -> Option<String> {
|
||||||
|
crate::core::service::take_service_owner_notice()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub fn sync_runtime_providers() {
|
pub fn sync_runtime_providers() {
|
||||||
if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
|
if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
|
||||||
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
|
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
||||||
|
mod tests {
|
||||||
|
use super::{ServiceInstallOutcome, finish_service_installation};
|
||||||
|
use clash_verge_service_ipc::{
|
||||||
|
CoreAvailability, CoreInspection, InstallationStatus, ProtocolInfo, management::InstallationVerificationError,
|
||||||
|
};
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rejected_installation_reports_sidecar_only_after_successful_continuation() {
|
||||||
|
let reason = "core path C:\\ is writable by Everyone";
|
||||||
|
for (availability, fallback_ready) in [
|
||||||
|
(None, false),
|
||||||
|
(Some(CoreAvailability::Missing), false),
|
||||||
|
(Some(CoreAvailability::DigestMismatch), false),
|
||||||
|
(Some(CoreAvailability::Rejected { reason: reason.into() }), false),
|
||||||
|
(Some(CoreAvailability::Rejected { reason: reason.into() }), true),
|
||||||
|
] {
|
||||||
|
let fallback_called = Cell::new(false);
|
||||||
|
let rejected = matches!(&availability, Some(CoreAvailability::Rejected { .. }));
|
||||||
|
let error = match availability {
|
||||||
|
Some(availability) => anyhow::Error::new(InstallationVerificationError {
|
||||||
|
status: InstallationStatus {
|
||||||
|
service_sha256: String::new(),
|
||||||
|
protocol: ProtocolInfo::current(),
|
||||||
|
cores: vec![CoreInspection {
|
||||||
|
name: "verge-mihomo.exe".into(),
|
||||||
|
availability,
|
||||||
|
}],
|
||||||
|
core_busy: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
None => anyhow::anyhow!("{reason}"),
|
||||||
|
}
|
||||||
|
.context("install service failed");
|
||||||
|
let result = finish_service_installation(Err(error), "SERVICE_INSTALL_FAILED", |_| async {
|
||||||
|
fallback_called.set(true);
|
||||||
|
anyhow::ensure!(fallback_ready, "another core is running");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(fallback_called.get(), rejected);
|
||||||
|
if rejected && fallback_ready {
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(ServiceInstallOutcome::Sidecar {
|
||||||
|
reason: format!("verge-mihomo.exe: {reason}")
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let error = result.expect_err("an unavailable fallback must remain an error");
|
||||||
|
assert_eq!(
|
||||||
|
error.code.as_deref(),
|
||||||
|
Some(if rejected {
|
||||||
|
"SERVICE_SIDECAR_FAILED"
|
||||||
|
} else {
|
||||||
|
"SERVICE_INSTALL_FAILED"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if rejected {
|
||||||
|
assert!(error.detail.contains("another core is running"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ use crate::core::{
|
|||||||
|
|
||||||
/// Returns one coherent core/service snapshot instead of independently refreshed state.
|
/// Returns one coherent core/service snapshot instead of independently refreshed state.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_runtime_state() -> Result<RunStateView, String> {
|
pub async fn get_runtime_state() -> Result<RunStateView, String> {
|
||||||
Ok(RUN_STATE.settled().await.to_view())
|
Ok(RUN_STATE.settled().await.to_view())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_pending_failures() -> Vec<PendingFailure> {
|
pub async fn get_pending_failures() -> Vec<PendingFailure> {
|
||||||
notification::pending_failures()
|
notification::pending_failures()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ mod platform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn invoke_uwp_tool() -> CmdResult {
|
pub async fn invoke_uwp_tool() -> CmdResult {
|
||||||
platform::invoke_uwp_tool()
|
platform::invoke_uwp_tool()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::core::notify::NoticeStatus;
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
handle,
|
handle,
|
||||||
validate::{ValidationErrorKind, ValidationOutcome},
|
validate::{ValidationErrorKind, ValidationOutcome},
|
||||||
@@ -11,27 +12,27 @@ pub enum ValidationNoticeTarget {
|
|||||||
Script,
|
Script,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn notice_key(kind: ValidationErrorKind, target: ValidationNoticeTarget) -> &'static str {
|
const fn notice_key(kind: ValidationErrorKind, target: ValidationNoticeTarget) -> NoticeStatus {
|
||||||
match kind {
|
match kind {
|
||||||
ValidationErrorKind::FileMissing => "config_validate::file_not_found",
|
ValidationErrorKind::FileMissing => NoticeStatus::ConfigValidateFileNotFound,
|
||||||
ValidationErrorKind::FileRead => match target {
|
ValidationErrorKind::FileRead => match target {
|
||||||
ValidationNoticeTarget::Script => "config_validate::script_error",
|
ValidationNoticeTarget::Script => NoticeStatus::ConfigValidateScriptError,
|
||||||
_ => "config_validate::yaml_read_error",
|
_ => NoticeStatus::ConfigValidateYamlReadError,
|
||||||
},
|
},
|
||||||
ValidationErrorKind::YamlSyntax => match target {
|
ValidationErrorKind::YamlSyntax => match target {
|
||||||
ValidationNoticeTarget::Merge => "config_validate::merge_syntax_error",
|
ValidationNoticeTarget::Merge => NoticeStatus::ConfigValidateMergeSyntaxError,
|
||||||
ValidationNoticeTarget::Script => "config_validate::script_error",
|
ValidationNoticeTarget::Script => NoticeStatus::ConfigValidateScriptError,
|
||||||
ValidationNoticeTarget::Runtime => "config_validate::yaml_syntax_error",
|
ValidationNoticeTarget::Runtime => NoticeStatus::ConfigValidateYamlSyntaxError,
|
||||||
},
|
},
|
||||||
ValidationErrorKind::YamlMapping => match target {
|
ValidationErrorKind::YamlMapping => match target {
|
||||||
ValidationNoticeTarget::Merge => "config_validate::merge_mapping_error",
|
ValidationNoticeTarget::Merge => NoticeStatus::ConfigValidateMergeMappingError,
|
||||||
ValidationNoticeTarget::Script => "config_validate::script_error",
|
ValidationNoticeTarget::Script => NoticeStatus::ConfigValidateScriptError,
|
||||||
ValidationNoticeTarget::Runtime => "config_validate::yaml_mapping_error",
|
ValidationNoticeTarget::Runtime => NoticeStatus::ConfigValidateYamlMappingError,
|
||||||
},
|
},
|
||||||
ValidationErrorKind::ScriptSyntax => "config_validate::script_syntax_error",
|
ValidationErrorKind::ScriptSyntax => NoticeStatus::ConfigValidateScriptSyntaxError,
|
||||||
ValidationErrorKind::ScriptMissingMain => "config_validate::script_missing_main",
|
ValidationErrorKind::ScriptMissingMain => NoticeStatus::ConfigValidateScriptMissingMain,
|
||||||
ValidationErrorKind::ProcessTerminated => "config_validate::process_terminated",
|
ValidationErrorKind::ProcessTerminated => NoticeStatus::ConfigValidateProcessTerminated,
|
||||||
ValidationErrorKind::CoreRejected | ValidationErrorKind::Timeout => "config_validate::error",
|
ValidationErrorKind::CoreRejected | ValidationErrorKind::Timeout => NoticeStatus::ConfigValidateError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,12 +41,12 @@ pub fn handle_validation_notice(outcome: &ValidationOutcome, target: ValidationN
|
|||||||
ValidationOutcome::Invalid { kind, message } => {
|
ValidationOutcome::Invalid { kind, message } => {
|
||||||
let status = notice_key(*kind, target);
|
let status = notice_key(*kind, target);
|
||||||
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, message);
|
logging!(warn, Type::Config, "{} 验证失败: {}", file_type, message);
|
||||||
handle::Handle::notice_message(status, message.to_owned());
|
handle::Handle::notice(status, message.as_str());
|
||||||
}
|
}
|
||||||
ValidationOutcome::Busy | ValidationOutcome::Skipped { .. } => {
|
ValidationOutcome::Busy | ValidationOutcome::Skipped { .. } => {
|
||||||
let message = outcome.to_string();
|
let message = outcome.to_string();
|
||||||
logging!(warn, Type::Config, "{} 验证跳过: {}", file_type, message);
|
logging!(warn, Type::Config, "{} 验证跳过: {}", file_type, message);
|
||||||
handle::Handle::notice_message("config_validate::error", message);
|
handle::Handle::notice(NoticeStatus::ConfigValidateError, message.as_str());
|
||||||
}
|
}
|
||||||
ValidationOutcome::Valid => {}
|
ValidationOutcome::Valid => {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ use clash_verge_draft::SharedDraft;
|
|||||||
|
|
||||||
/// 获取Verge配置
|
/// 获取Verge配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
|
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
|
||||||
feat::fetch_verge_config().await.stringify_err()
|
feat::fetch_verge_config().await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改Verge配置
|
/// 修改Verge配置
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||||
let operation = system_proxy_operation(&payload);
|
let operation = system_proxy_operation(&payload);
|
||||||
let result = match operation {
|
let result = match operation {
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ use crate::{
|
|||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core, feat,
|
core, feat,
|
||||||
};
|
};
|
||||||
use reqwest_dav::list_cmd::ListFile;
|
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
|
pub async fn save_webdav_config(
|
||||||
|
url: std::string::String,
|
||||||
|
username: std::string::String,
|
||||||
|
password: std::string::String,
|
||||||
|
) -> CmdResult<()> {
|
||||||
|
let url: String = url.into();
|
||||||
|
let username: String = username.into();
|
||||||
|
let password: String = password.into();
|
||||||
let patch = IVerge {
|
let patch = IVerge {
|
||||||
webdav_url: Some(url),
|
webdav_url: Some(url),
|
||||||
webdav_username: Some(username),
|
webdav_username: Some(username),
|
||||||
@@ -25,22 +32,31 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
pub async fn create_webdav_backup() -> CmdResult<()> {
|
pub async fn create_webdav_backup() -> CmdResult<()> {
|
||||||
feat::create_backup_and_upload_webdav().await.stringify_err()
|
feat::create_backup_and_upload_webdav().await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
|
#[specta::specta]
|
||||||
feat::list_wevdav_backup().await.stringify_err()
|
pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
|
||||||
|
feat::list_wevdav_backup()
|
||||||
|
.await
|
||||||
|
.map(|files| files.into_iter().map(Into::into).collect())
|
||||||
|
.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
|
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||||
|
let filename: String = filename.into();
|
||||||
feat::delete_webdav_backup(filename).await.stringify_err()
|
feat::delete_webdav_backup(filename).await.stringify_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
|
#[specta::specta]
|
||||||
|
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
|
||||||
|
let filename: String = filename.into();
|
||||||
feat::restore_webdav_backup(filename)
|
feat::restore_webdav_backup(filename)
|
||||||
.await
|
.await
|
||||||
.map_err(|error| super::proxy_aware_error(&error))
|
.map_err(|error| super::proxy_aware_error(&error))
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ impl IClashTemp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
pub struct ClashInfo {
|
pub struct ClashInfo {
|
||||||
/// clash core port
|
/// clash core port
|
||||||
pub mixed_port: u16,
|
pub mixed_port: u16,
|
||||||
@@ -364,14 +364,14 @@ fn test_clash_info() {
|
|||||||
assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097"));
|
assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClashExternalControllerCors {
|
pub struct IClashExternalControllerCors {
|
||||||
pub allow_origins: Option<Vec<String>>,
|
pub allow_origins: Option<Vec<String>>,
|
||||||
pub allow_private_network: Option<bool>,
|
pub allow_private_network: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClash {
|
pub struct IClash {
|
||||||
pub mixed_port: Option<u16>,
|
pub mixed_port: Option<u16>,
|
||||||
@@ -387,7 +387,7 @@ pub struct IClash {
|
|||||||
pub external_controller_cors: Option<IClashExternalControllerCors>,
|
pub external_controller_cors: Option<IClashExternalControllerCors>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClashTUN {
|
pub struct IClashTUN {
|
||||||
pub enable: Option<bool>,
|
pub enable: Option<bool>,
|
||||||
@@ -397,7 +397,7 @@ pub struct IClashTUN {
|
|||||||
pub dns_hijack: Option<Vec<String>>,
|
pub dns_hijack: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClashDNS {
|
pub struct IClashDNS {
|
||||||
pub enable: Option<bool>,
|
pub enable: Option<bool>,
|
||||||
@@ -414,7 +414,7 @@ pub struct IClashDNS {
|
|||||||
pub nameserver_policy: Option<Vec<String>>,
|
pub nameserver_policy: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct IClashFallbackFilter {
|
pub struct IClashFallbackFilter {
|
||||||
pub geoip: Option<bool>,
|
pub geoip: Option<bool>,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::{IClashTemp, IProfiles, IVerge, MixedPort};
|
use super::{IClashTemp, IProfiles, IVerge, MixedPort};
|
||||||
|
use crate::core::notify::NoticeStatus;
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{PrfItem, profiles_append_item_to_safe, runtime::IRuntime},
|
config::{PrfItem, profiles_append_item_to_safe, runtime::IRuntime},
|
||||||
constants::{files, timing},
|
constants::{files, timing},
|
||||||
@@ -6,6 +7,7 @@ use crate::{
|
|||||||
CoreManager,
|
CoreManager,
|
||||||
handle::{self, Handle},
|
handle::{self, Handle},
|
||||||
listener::MIXED_PORT_KEY,
|
listener::MIXED_PORT_KEY,
|
||||||
|
runtime_bundle::resolve_provider_path_conflicts,
|
||||||
tray,
|
tray,
|
||||||
validate::CoreConfigValidator,
|
validate::CoreConfigValidator,
|
||||||
},
|
},
|
||||||
@@ -68,6 +70,12 @@ impl Config {
|
|||||||
CONFIG_WRITE_LOCK.lock().await
|
CONFIG_WRITE_LOCK.lock().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_lock_config_write() -> Result<MutexGuard<'static, ()>> {
|
||||||
|
CONFIG_WRITE_LOCK
|
||||||
|
.try_lock()
|
||||||
|
.map_err(|_| anyhow!("configuration update is already running"))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init_config_before_window() -> Result<()> {
|
pub async fn init_config_before_window() -> Result<()> {
|
||||||
Self::ensure_default_profile_items().await?;
|
Self::ensure_default_profile_items().await?;
|
||||||
|
|
||||||
@@ -105,7 +113,7 @@ impl Config {
|
|||||||
|
|
||||||
if let Some((msg_type, msg_content)) = validation_result {
|
if let Some((msg_type, msg_content)) = validation_result {
|
||||||
sleep(timing::STARTUP_ERROR_DELAY).await;
|
sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||||
handle::Handle::notice_message(msg_type, msg_content);
|
handle::Handle::notice(msg_type, msg_content.as_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::runtime().await.apply();
|
Self::runtime().await.apply();
|
||||||
@@ -136,14 +144,14 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate_and_validate() -> Result<Option<(&'static str, String)>> {
|
async fn generate_and_validate() -> Result<Option<(NoticeStatus, String)>> {
|
||||||
if let Err(err) = Self::generate().await {
|
if let Err(err) = Self::generate().await {
|
||||||
let error_msg: String = err.to_string().into();
|
let error_msg: String = err.to_string().into();
|
||||||
logging!(error, Type::Config, "生成运行时配置失败: {}", error_msg);
|
logging!(error, Type::Config, "生成运行时配置失败: {}", error_msg);
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::boot_error", &error_msg)
|
.use_default_config(NoticeStatus::ConfigValidateBootError, &error_msg)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(Some(("config_validate::boot_error", error_msg)));
|
return Ok(Some((NoticeStatus::ConfigValidateBootError, error_msg)));
|
||||||
}
|
}
|
||||||
logging!(debug, Type::Config, "生成运行时配置成功");
|
logging!(debug, Type::Config, "生成运行时配置成功");
|
||||||
|
|
||||||
@@ -170,25 +178,25 @@ impl Config {
|
|||||||
error_msg
|
error_msg
|
||||||
);
|
);
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::boot_error", &error_msg)
|
.use_default_config(NoticeStatus::ConfigValidateBootError, &error_msg)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(("config_validate::boot_error", error_msg)))
|
Ok(Some((NoticeStatus::ConfigValidateBootError, error_msg)))
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logging!(warn, Type::Config, "验证过程执行失败: {err:#}");
|
logging!(warn, Type::Config, "验证过程执行失败: {err:#}");
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::process_terminated", "")
|
.use_default_config(NoticeStatus::ConfigValidateProcessTerminated, "")
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(("config_validate::process_terminated", String::new())))
|
Ok(Some((NoticeStatus::ConfigValidateProcessTerminated, String::new())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default();
|
let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default();
|
||||||
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}");
|
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}");
|
||||||
CoreManager::global()
|
CoreManager::global()
|
||||||
.use_default_config("config_validate::error", "")
|
.use_default_config(NoticeStatus::ConfigValidateError, "")
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(("config_validate::error", String::new())))
|
Ok(Some((NoticeStatus::ConfigValidateError, String::new())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +236,7 @@ impl Config {
|
|||||||
pub(crate) async fn generate_with_profiles(profiles: &IProfiles) -> Result<()> {
|
pub(crate) async fn generate_with_profiles(profiles: &IProfiles) -> Result<()> {
|
||||||
let (mut config, exists_keys, logs, dns_override) = enhance::enhance(profiles).await?;
|
let (mut config, exists_keys, logs, dns_override) = enhance::enhance(profiles).await?;
|
||||||
|
|
||||||
|
resolve_provider_path_conflicts(&mut config, &dirs::app_home_dir()?)?;
|
||||||
sanitize_tunnels_proxy(&mut config);
|
sanitize_tunnels_proxy(&mut config);
|
||||||
// Apply only to generated core config so the saved choice survives the next launch.
|
// Apply only to generated core config so the saved choice survives the next launch.
|
||||||
if let Some(port) = MixedPort::session_fallback() {
|
if let Some(port) = MixedPort::session_fallback() {
|
||||||
|
|||||||
+139
-38
@@ -1,5 +1,7 @@
|
|||||||
|
use crate::core::notify::NoticeStatus;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clash_verge_draft::DraftTransaction;
|
use clash_verge_draft::DraftTransaction;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml_ng::{Mapping, Value};
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use sha2::{Digest as _, Sha256};
|
use sha2::{Digest as _, Sha256};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
@@ -54,6 +56,7 @@ pub(crate) fn dns_override_source(profile_uid: &str, config: &Mapping) -> Result
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct DnsOverrideState {
|
pub(crate) struct DnsOverrideState {
|
||||||
|
profile_uid: String,
|
||||||
pub source: Option<String>,
|
pub source: Option<String>,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
requested: bool,
|
requested: bool,
|
||||||
@@ -61,9 +64,10 @@ pub(crate) struct DnsOverrideState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DnsOverrideState {
|
impl DnsOverrideState {
|
||||||
pub fn new(source: Option<String>, requested: bool, confirmation: Option<String>) -> Self {
|
pub fn new(profile_uid: &str, source: Option<String>, requested: bool, confirmation: Option<String>) -> Self {
|
||||||
let enabled = requested && (source.is_none() || source == confirmation);
|
let enabled = requested && (source.is_none() || source == confirmation);
|
||||||
Self {
|
Self {
|
||||||
|
profile_uid: profile_uid.into(),
|
||||||
source,
|
source,
|
||||||
enabled,
|
enabled,
|
||||||
requested,
|
requested,
|
||||||
@@ -72,24 +76,56 @@ impl DnsOverrideState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_to(&self, verge: &mut IVerge) -> bool {
|
fn apply_to(&self, verge: &mut IVerge) -> bool {
|
||||||
|
if self.profile_uid.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let settings = verge.dns_settings_for(&self.profile_uid);
|
||||||
// A later settings write must not be overwritten by an earlier runtime generation.
|
// A later settings write must not be overwritten by an earlier runtime generation.
|
||||||
if verge.enable_dns_settings.unwrap_or(false) != self.requested
|
if (settings.enabled, settings.confirmation.as_ref()) != (self.requested, self.confirmation.as_ref()) {
|
||||||
|| verge.dns_override_confirmation != self.confirmation
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let clear_confirmation = self.confirmation.is_some() && self.source != self.confirmation;
|
let clear_confirmation = self.confirmation.is_some() && self.source != self.confirmation;
|
||||||
if self.enabled == self.requested && !clear_confirmation {
|
if self.enabled == self.requested
|
||||||
|
&& !clear_confirmation
|
||||||
|
&& verge.profile_dns_settings.contains_key(&self.profile_uid)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
verge.enable_dns_settings = Some(self.enabled);
|
verge.profile_dns_settings.insert(
|
||||||
if clear_confirmation {
|
self.profile_uid.clone(),
|
||||||
verge.dns_override_confirmation = None;
|
ProfileDnsSettings {
|
||||||
}
|
enabled: self.enabled,
|
||||||
|
confirmation: if clear_confirmation {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.confirmation.clone()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize, Serialize, specta::Type)]
|
||||||
|
pub struct ProfileDnsSettings {
|
||||||
|
pub enabled: bool,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
|
pub confirmation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IVerge {
|
||||||
|
pub(crate) fn dns_settings_for(&self, profile_uid: &str) -> ProfileDnsSettings {
|
||||||
|
self.profile_dns_settings
|
||||||
|
.get(profile_uid)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(ProfileDnsSettings {
|
||||||
|
enabled: self.enable_dns_settings.unwrap_or(false),
|
||||||
|
confirmation: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub(crate) async fn sync_dns_override() -> Result<()> {
|
pub(crate) async fn sync_dns_override() -> Result<()> {
|
||||||
let _config_write = Self::lock_config_write().await;
|
let _config_write = Self::lock_config_write().await;
|
||||||
@@ -104,19 +140,20 @@ impl Config {
|
|||||||
}
|
}
|
||||||
// The runtime is already committed; a disk failure cannot undo its effective settings.
|
// The runtime is already committed; a disk failure cannot undo its effective settings.
|
||||||
transaction.commit();
|
transaction.commit();
|
||||||
Handle::refresh_verge();
|
|
||||||
if state.requested && !state.enabled {
|
if state.requested && !state.enabled {
|
||||||
PENDING_DNS_OVERRIDE_NOTICE.store(true, Ordering::Relaxed);
|
PENDING_DNS_OVERRIDE_NOTICE.store(true, Ordering::Relaxed);
|
||||||
Handle::notice_message("dns_override::auto_disabled", "");
|
Handle::notice(NoticeStatus::DnsOverrideAutoDisabled, "");
|
||||||
}
|
}
|
||||||
verge.data_arc().save_file().await
|
let result = verge.data_arc().save_file().await;
|
||||||
|
Handle::refresh_verge();
|
||||||
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DnsOverrideState, dns_override_source};
|
use super::{DnsOverrideState, ProfileDnsSettings, dns_override_source};
|
||||||
use crate::config::IVerge;
|
use crate::config::IVerge;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde_yaml_ng::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
@@ -152,35 +189,46 @@ mod tests {
|
|||||||
let reordered =
|
let reordered =
|
||||||
mapping("dns:\n nameserver-policy:\n b.example: [8.8.8.8]\n a.example: 1.1.1.1\nport: 7890");
|
mapping("dns:\n nameserver-policy:\n b.example: [8.8.8.8]\n a.example: 1.1.1.1\nport: 7890");
|
||||||
assert_eq!(source, dns_override_source("one", &reordered)?);
|
assert_eq!(source, dns_override_source("one", &reordered)?);
|
||||||
assert!(DnsOverrideState::new(source.clone(), true, source.clone()).enabled);
|
assert!(DnsOverrideState::new("one", source.clone(), true, source.clone()).enabled);
|
||||||
assert!(!DnsOverrideState::new(dns_override_source("two", &original)?, true, source.clone()).enabled);
|
assert!(!DnsOverrideState::new("two", dns_override_source("two", &original)?, true, source.clone()).enabled);
|
||||||
let changed = mapping("dns: {nameserver-policy: {a.example: 9.9.9.9, b.example: [8.8.8.8]}}");
|
let changed = mapping("dns: {nameserver-policy: {a.example: 9.9.9.9, b.example: [8.8.8.8]}}");
|
||||||
assert!(!DnsOverrideState::new(dns_override_source("one", &changed)?, true, source).enabled);
|
assert!(!DnsOverrideState::new("one", dns_override_source("one", &changed)?, true, source).enabled);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dns_override_confirmation_expires_on_restart() -> Result<()> {
|
fn dns_override_confirmation_survives_restart_only_for_the_same_source() -> Result<()> {
|
||||||
let source = Some("provider-dns".into());
|
let source = Some("provider-dns".into());
|
||||||
let confirmed = IVerge {
|
let confirmed = IVerge {
|
||||||
enable_dns_settings: Some(true),
|
profile_dns_settings: [(
|
||||||
dns_override_confirmation: source.clone(),
|
"one".into(),
|
||||||
|
ProfileDnsSettings {
|
||||||
|
enabled: true,
|
||||||
|
confirmation: source.clone(),
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
.into(),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
};
|
};
|
||||||
assert!(DnsOverrideState::new(source.clone(), true, confirmed.dns_override_confirmation.clone()).enabled);
|
let saved = serde_yaml_ng::to_string(&confirmed)?;
|
||||||
for saved in [
|
let mut restarted: IVerge = serde_yaml_ng::from_str(&saved)?;
|
||||||
serde_yaml_ng::to_string(&confirmed)?,
|
let settings = restarted.dns_settings_for("one");
|
||||||
"enable_dns_settings: true\ndns_override_confirmation: provider-dns".to_owned(),
|
let state = DnsOverrideState::new("one", source.clone(), settings.enabled, settings.confirmation);
|
||||||
|
assert!(state.enabled);
|
||||||
|
assert!(!state.apply_to(&mut restarted));
|
||||||
|
for (saved, source) in [
|
||||||
|
(saved, Some("updated".into())),
|
||||||
|
(
|
||||||
|
"enable_dns_settings: true\ndns_override_confirmation: provider-dns".to_owned(),
|
||||||
|
source,
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let mut restarted: IVerge = serde_yaml_ng::from_str(&saved)?;
|
let mut restarted: IVerge = serde_yaml_ng::from_str(&saved)?;
|
||||||
let state = DnsOverrideState::new(
|
let settings = restarted.dns_settings_for("one");
|
||||||
source.clone(),
|
let state = DnsOverrideState::new("one", source, settings.enabled, settings.confirmation);
|
||||||
restarted.enable_dns_settings.unwrap_or(false),
|
|
||||||
restarted.dns_override_confirmation.clone(),
|
|
||||||
);
|
|
||||||
assert!(!state.enabled, "a previous session must not bypass startup protection");
|
assert!(!state.enabled, "a previous session must not bypass startup protection");
|
||||||
assert!(state.apply_to(&mut restarted));
|
assert!(state.apply_to(&mut restarted));
|
||||||
assert_eq!(restarted.enable_dns_settings, Some(false));
|
assert!(!restarted.dns_settings_for("one").enabled);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -191,21 +239,74 @@ mod tests {
|
|||||||
enable_dns_settings: Some(true),
|
enable_dns_settings: Some(true),
|
||||||
..IVerge::default()
|
..IVerge::default()
|
||||||
};
|
};
|
||||||
let state = DnsOverrideState::new(Some("provider-dns".into()), true, None);
|
let state = DnsOverrideState::new("one", Some("provider-dns".into()), true, None);
|
||||||
assert!(state.apply_to(&mut verge));
|
assert!(state.apply_to(&mut verge));
|
||||||
assert_eq!(verge.enable_dns_settings, Some(false));
|
assert!(!verge.dns_settings_for("one").enabled);
|
||||||
assert!(!state.apply_to(&mut verge));
|
assert!(!state.apply_to(&mut verge));
|
||||||
let disabled = DnsOverrideState::new(None, false, None);
|
let disabled = DnsOverrideState::new("one", None, false, None);
|
||||||
assert!(!disabled.apply_to(&mut verge));
|
assert!(!disabled.apply_to(&mut verge));
|
||||||
assert!(!disabled.enabled);
|
assert!(!disabled.enabled);
|
||||||
|
|
||||||
verge.enable_dns_settings = Some(true);
|
verge.profile_dns_settings.insert(
|
||||||
verge.dns_override_confirmation = Some("provider-dns".into());
|
"one".into(),
|
||||||
|
ProfileDnsSettings {
|
||||||
|
enabled: true,
|
||||||
|
confirmation: Some("provider-dns".into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
assert!(!state.apply_to(&mut verge));
|
assert!(!state.apply_to(&mut verge));
|
||||||
assert_eq!(verge.enable_dns_settings, Some(true));
|
let settings = verge.dns_settings_for("one");
|
||||||
assert_eq!(verge.dns_override_confirmation.as_deref(), Some("provider-dns"));
|
assert!(settings.enabled);
|
||||||
let leaving = DnsOverrideState::new(None, true, verge.dns_override_confirmation.clone());
|
assert_eq!(settings.confirmation.as_deref(), Some("provider-dns"));
|
||||||
|
let leaving = DnsOverrideState::new("one", None, true, settings.confirmation);
|
||||||
assert!(leaving.apply_to(&mut verge));
|
assert!(leaving.apply_to(&mut verge));
|
||||||
assert!(verge.dns_override_confirmation.is_none());
|
assert!(verge.dns_settings_for("one").confirmation.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn automatic_disable_is_saved_only_for_the_affected_profile() -> Result<()> {
|
||||||
|
let mut verge = IVerge {
|
||||||
|
enable_dns_settings: Some(true),
|
||||||
|
..IVerge::default()
|
||||||
|
};
|
||||||
|
let first = DnsOverrideState::new("one", None, true, None);
|
||||||
|
assert!(first.apply_to(&mut verge));
|
||||||
|
let protected = DnsOverrideState::new("two", Some("provider-dns".into()), true, None);
|
||||||
|
assert!(protected.apply_to(&mut verge));
|
||||||
|
assert!(verge.dns_settings_for("one").enabled);
|
||||||
|
assert!(!verge.dns_settings_for("two").enabled);
|
||||||
|
assert!(verge.dns_settings_for("unvisited").enabled);
|
||||||
|
assert_eq!(verge.enable_dns_settings, Some(true));
|
||||||
|
let restarted: IVerge = serde_yaml_ng::from_str(&serde_yaml_ng::to_string(&verge)?)?;
|
||||||
|
assert!(restarted.dns_settings_for("one").enabled);
|
||||||
|
assert!(!restarted.dns_settings_for("two").enabled);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switching_profiles_preserves_each_confirmation_until_its_source_changes() {
|
||||||
|
let mut verge = IVerge::default();
|
||||||
|
for uid in ["one", "two"] {
|
||||||
|
verge.profile_dns_settings.insert(
|
||||||
|
uid.into(),
|
||||||
|
ProfileDnsSettings {
|
||||||
|
enabled: true,
|
||||||
|
confirmation: Some(uid.into()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for uid in ["one", "two", "one"] {
|
||||||
|
let settings = verge.dns_settings_for(uid);
|
||||||
|
let state = DnsOverrideState::new(uid, Some(uid.into()), settings.enabled, settings.confirmation);
|
||||||
|
assert!(state.enabled);
|
||||||
|
assert!(!state.apply_to(&mut verge));
|
||||||
|
}
|
||||||
|
let settings = verge.dns_settings_for("two");
|
||||||
|
let updated = DnsOverrideState::new("two", Some("updated".into()), settings.enabled, settings.confirmation);
|
||||||
|
assert!(updated.apply_to(&mut verge));
|
||||||
|
assert!(!verge.dns_settings_for("two").enabled);
|
||||||
|
assert!(verge.dns_settings_for("two").confirmation.is_none());
|
||||||
|
assert!(verge.dns_settings_for("one").enabled);
|
||||||
|
assert_eq!(verge.dns_settings_for("one").confirmation.as_deref(), Some("one"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::{Config, IClashTemp, IVerge, MixedPort};
|
use super::{Config, IClashTemp, IVerge, MixedPort};
|
||||||
|
use crate::core::notify::NoticeStatus;
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::timing,
|
constants::timing,
|
||||||
core::{
|
core::{
|
||||||
@@ -14,15 +15,9 @@ use crate::{
|
|||||||
use anyhow::{Context as _, Result, anyhow, bail};
|
use anyhow::{Context as _, Result, anyhow, bail};
|
||||||
use clash_verge_draft::DraftTransaction;
|
use clash_verge_draft::DraftTransaction;
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use serde_yaml_ng::Value;
|
use serde_yaml_ng::Value;
|
||||||
use std::{
|
use std::{collections::HashSet, net::SocketAddr, str::FromStr as _};
|
||||||
collections::HashSet,
|
|
||||||
net::SocketAddr,
|
|
||||||
str::FromStr as _,
|
|
||||||
sync::atomic::{AtomicBool, Ordering},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct MixedPortFallback {
|
struct MixedPortFallback {
|
||||||
@@ -30,9 +25,8 @@ struct MixedPortFallback {
|
|||||||
current: u16,
|
current: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
static PENDING_FALLBACK_NOTICE: Lazy<Mutex<Option<MixedPortFallback>>> = Lazy::new(|| Mutex::new(None));
|
static PENDING_FALLBACK_NOTICE: Mutex<Option<MixedPortFallback>> = Mutex::new(None);
|
||||||
static STARTUP_CORE_BLOCKED: AtomicBool = AtomicBool::new(false);
|
static STARTUP_CORE_BLOCK_REASON: Mutex<Option<String>> = Mutex::new(None);
|
||||||
static STARTUP_CORE_BLOCK_REASON: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
|
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub(crate) async fn resolve_startup_mixed_port() -> Result<bool> {
|
pub(crate) async fn resolve_startup_mixed_port() -> Result<bool> {
|
||||||
@@ -147,17 +141,12 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn block_startup_core(error: &anyhow::Error) {
|
pub(crate) fn block_startup_core(error: &anyhow::Error) {
|
||||||
STARTUP_CORE_BLOCKED.store(true, Ordering::Release);
|
|
||||||
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
|
*STARTUP_CORE_BLOCK_REASON.lock() = Some(error.to_string());
|
||||||
report_fallback_error(error.to_string());
|
report_fallback_error(error.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn startup_core_block_reason() -> Option<String> {
|
pub(crate) fn startup_core_block_reason() -> Option<String> {
|
||||||
if STARTUP_CORE_BLOCKED.load(Ordering::Acquire) {
|
STARTUP_CORE_BLOCK_REASON.lock().clone()
|
||||||
STARTUP_CORE_BLOCK_REASON.lock().clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn notify_startup_mixed_port_fallback() {
|
pub(crate) fn notify_startup_mixed_port_fallback() {
|
||||||
@@ -166,8 +155,8 @@ impl Config {
|
|||||||
};
|
};
|
||||||
AsyncHandler::spawn(move || async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
tokio::time::sleep(timing::STARTUP_ERROR_DELAY).await;
|
tokio::time::sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||||
Handle::notice_message(
|
Handle::notice(
|
||||||
"mixed_port::fallback",
|
NoticeStatus::MixedPortFallback,
|
||||||
format!("{},{}", change.original, change.current),
|
format!("{},{}", change.original, change.current),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -196,7 +185,7 @@ fn report_fallback_error(message: String) {
|
|||||||
);
|
);
|
||||||
AsyncHandler::spawn(move || async move {
|
AsyncHandler::spawn(move || async move {
|
||||||
tokio::time::sleep(timing::STARTUP_ERROR_DELAY).await;
|
tokio::time::sleep(timing::STARTUP_ERROR_DELAY).await;
|
||||||
Handle::notice_message("mixed_port::fallback_error", message);
|
Handle::notice(NoticeStatus::MixedPortFallbackError, message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,27 +27,36 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
|
|||||||
Some(url.to_string().into())
|
Some(url.to_string().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct PrfItem {
|
pub struct PrfItem {
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub uid: Option<String>,
|
pub uid: Option<String>,
|
||||||
|
|
||||||
/// enum value: remote | local | script | merge
|
/// enum value: remote | local | script | merge
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub itype: Option<String>,
|
pub itype: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub file: Option<String>,
|
pub file: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub selected: Option<Vec<PrfSelected>>,
|
pub selected: Option<Vec<PrfSelected>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::collections::BTreeMap<std::string::String, std::string::String>>)]
|
||||||
|
pub latency_test_urls: Option<std::collections::BTreeMap<String, String>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub extra: Option<PrfExtra>,
|
pub extra: Option<PrfExtra>,
|
||||||
|
|
||||||
@@ -57,19 +66,24 @@ pub struct PrfItem {
|
|||||||
pub option: Option<PrfOption>,
|
pub option: Option<PrfOption>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub home: Option<String>,
|
pub home: Option<String>,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub file_data: Option<String>,
|
pub file_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct PrfSelected {
|
pub struct PrfSelected {
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub now: Option<String>,
|
pub now: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, specta::Type)]
|
||||||
pub struct PrfExtra {
|
pub struct PrfExtra {
|
||||||
pub upload: u64,
|
pub upload: u64,
|
||||||
pub download: u64,
|
pub download: u64,
|
||||||
@@ -77,9 +91,11 @@ pub struct PrfExtra {
|
|||||||
pub expire: u64,
|
pub expire: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct PrfOption {
|
pub struct PrfOption {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub user_agent: Option<String>,
|
pub user_agent: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -102,15 +118,15 @@ pub struct PrfOption {
|
|||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub allow_auto_update: Option<bool>,
|
pub allow_auto_update: Option<bool>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub merge: Option<String>,
|
pub merge: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub script: Option<String>,
|
pub script: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub rules: Option<String>,
|
pub rules: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub proxies: Option<String>,
|
pub proxies: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub groups: Option<String>,
|
pub groups: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +233,7 @@ impl PrfItem {
|
|||||||
desc: Some(desc),
|
desc: Some(desc),
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
|
latency_test_urls: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
@@ -396,6 +413,7 @@ impl PrfItem {
|
|||||||
desc: desc.cloned(),
|
desc: desc.cloned(),
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: Some(url.as_str().into()),
|
url: Some(url.as_str().into()),
|
||||||
|
latency_test_urls: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra,
|
extra,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
|
|||||||
+167
-151
@@ -2,6 +2,8 @@ use super::{
|
|||||||
PrfOption,
|
PrfOption,
|
||||||
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
|
prfitem::{PrfItem, PrfSelected, normalize_profile_home_url},
|
||||||
};
|
};
|
||||||
|
use crate::core::notify::{Refresh, announce};
|
||||||
|
use crate::utils::retry::{RetryError, RetryPolicy, retry, retry_with_state};
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{handle, tray::Tray},
|
core::{handle, tray::Tray},
|
||||||
utils::{
|
utils::{
|
||||||
@@ -16,6 +18,7 @@ use parking_lot::Mutex;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml_ng::Mapping;
|
use serde_yaml_ng::Mapping;
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::{Component, Path},
|
path::{Component, Path},
|
||||||
@@ -45,8 +48,10 @@ const SELECTED_NODES_SETTLE_INTERVAL: Duration = Duration::from_secs(1);
|
|||||||
const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
|
const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
|
||||||
|
|
||||||
/// Define the `profiles.yaml` schema
|
/// Define the `profiles.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct IProfiles {
|
pub struct IProfiles {
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub current: Option<String>,
|
pub current: Option<String>,
|
||||||
|
|
||||||
pub items: Option<Vec<PrfItem>>,
|
pub items: Option<Vec<PrfItem>>,
|
||||||
@@ -68,8 +73,14 @@ impl ProfileDeletePlan {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
for file in self.files {
|
for file in self.files {
|
||||||
if let Err(error) = dir.join(file.as_str()).remove_if_exists().await {
|
let path = dir.join(file.as_str());
|
||||||
logging!(warn, Type::Config, "清理已删除订阅文件失败: {file} - {error}");
|
if let Err(error) = path.remove_if_exists().await {
|
||||||
|
logging!(
|
||||||
|
warn,
|
||||||
|
Type::Config,
|
||||||
|
"Failed to remove deleted profile file {}: {error:#}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,6 +250,7 @@ impl IProfiles {
|
|||||||
patch!(each, item, file);
|
patch!(each, item, file);
|
||||||
patch!(each, item, url);
|
patch!(each, item, url);
|
||||||
patch!(each, item, selected);
|
patch!(each, item, selected);
|
||||||
|
patch!(each, item, latency_test_urls);
|
||||||
patch!(each, item, extra);
|
patch!(each, item, extra);
|
||||||
patch!(each, item, updated);
|
patch!(each, item, updated);
|
||||||
patch!(each, item, option);
|
patch!(each, item, option);
|
||||||
@@ -594,19 +606,21 @@ fn is_activation_current(generation: u64) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_proxies_with_timeout() -> Result<Proxies> {
|
async fn fetch_proxies_with_timeout() -> Result<Proxies> {
|
||||||
tokio::time::timeout(MIHOMO_OPERATION_TIMEOUT, async {
|
let interval = Duration::from_millis(500);
|
||||||
loop {
|
let attempts =
|
||||||
match handle::Handle::mihomo().get_proxies().await {
|
NonZeroUsize::MIN.saturating_add((MIHOMO_OPERATION_TIMEOUT.as_millis() / interval.as_millis()) as usize + 1);
|
||||||
Ok(proxies) => return proxies,
|
tokio::time::timeout(
|
||||||
Err(err) => {
|
MIHOMO_OPERATION_TIMEOUT,
|
||||||
logging!(debug, Type::Config, "mihomo proxies are not ready yet: {err:#}");
|
retry(RetryPolicy::fixed(attempts, interval), |_| async {
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
handle::Handle::mihomo().get_proxies().await.map_err(|error| {
|
||||||
}
|
logging!(debug, Type::Config, "mihomo proxies are not ready yet: {error:#}");
|
||||||
}
|
RetryError::Retry(error)
|
||||||
}
|
})
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.context("timed out while waiting for mihomo proxies")
|
.context("timed out while waiting for mihomo proxies")?
|
||||||
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn select_node_with_timeout(group_name: &String, node: &String) -> Result<()> {
|
async fn select_node_with_timeout(group_name: &String, node: &String) -> Result<()> {
|
||||||
@@ -685,51 +699,58 @@ async fn update_tray_after_activation(generation: u64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn persist_selected_update<F, Fut>(supersede: bool, edit: F) -> Result<()>
|
||||||
|
where
|
||||||
|
F: FnOnce(IProfiles) -> Fut + Send,
|
||||||
|
Fut: std::future::Future<Output = Result<(IProfiles, bool)>> + Send,
|
||||||
|
{
|
||||||
|
let changed = Config::profiles().await.with_data_modify(edit).await?;
|
||||||
|
if changed {
|
||||||
|
if supersede {
|
||||||
|
supersede_selected_activation();
|
||||||
|
}
|
||||||
|
announce(Refresh::Profiles);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Records a backend-made selection so the next core start restores it.
|
/// Records a backend-made selection so the next core start restores it.
|
||||||
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
|
pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> {
|
||||||
let group_name = String::from(group_name);
|
let group_name = String::from(group_name);
|
||||||
let node = String::from(node);
|
let node = String::from(node);
|
||||||
let recorded = Config::profiles()
|
persist_selected_update(true, move |mut profiles| async move {
|
||||||
.await
|
let Some(current) = profiles.current.clone() else {
|
||||||
.with_data_modify(move |mut profiles| async move {
|
return Ok((profiles, false));
|
||||||
let Some(current) = profiles.current.clone() else {
|
};
|
||||||
return Ok((profiles, false));
|
let Some(item) = profiles
|
||||||
};
|
.items
|
||||||
let Some(item) = profiles
|
.as_mut()
|
||||||
.items
|
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||||
.as_mut()
|
else {
|
||||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
return Ok((profiles, false));
|
||||||
else {
|
};
|
||||||
return Ok((profiles, false));
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut selected = item.selected.clone().unwrap_or_default();
|
let mut selected = item.selected.clone().unwrap_or_default();
|
||||||
match selected
|
match selected
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|entry| entry.name.as_ref() == Some(&group_name))
|
.find(|entry| entry.name.as_ref() == Some(&group_name))
|
||||||
{
|
{
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
if entry.now.as_ref() == Some(&node) {
|
if entry.now.as_ref() == Some(&node) {
|
||||||
return Ok((profiles, false));
|
return Ok((profiles, false));
|
||||||
}
|
|
||||||
entry.now = Some(node);
|
|
||||||
}
|
}
|
||||||
None => selected.push(PrfSelected {
|
entry.now = Some(node);
|
||||||
name: Some(group_name),
|
|
||||||
now: Some(node),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
item.selected = Some(selected);
|
None => selected.push(PrfSelected {
|
||||||
profiles.save_file().await?;
|
name: Some(group_name),
|
||||||
Ok((profiles, true))
|
now: Some(node),
|
||||||
})
|
}),
|
||||||
.await?;
|
}
|
||||||
|
item.selected = Some(selected);
|
||||||
if recorded {
|
profiles.save_file().await?;
|
||||||
supersede_selected_activation();
|
Ok((profiles, true))
|
||||||
handle::Handle::refresh_profiles();
|
})
|
||||||
}
|
.await
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bool {
|
fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bool {
|
||||||
@@ -739,36 +760,28 @@ fn remove_selected_node(selected: &mut Vec<PrfSelected>, group_name: &str) -> bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
|
pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> {
|
||||||
let cleared = Config::profiles()
|
persist_selected_update(true, move |mut profiles| async move {
|
||||||
.await
|
let Some(current) = profiles.current.clone() else {
|
||||||
.with_data_modify(move |mut profiles| async move {
|
return Ok((profiles, false));
|
||||||
let Some(current) = profiles.current.clone() else {
|
};
|
||||||
return Ok((profiles, false));
|
let Some(item) = profiles
|
||||||
};
|
.items
|
||||||
let Some(item) = profiles
|
.as_mut()
|
||||||
.items
|
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
||||||
.as_mut()
|
else {
|
||||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t)))
|
return Ok((profiles, false));
|
||||||
else {
|
};
|
||||||
return Ok((profiles, false));
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut selected = item.selected.clone().unwrap_or_default();
|
let mut selected = item.selected.clone().unwrap_or_default();
|
||||||
if !remove_selected_node(&mut selected, group_name) {
|
if !remove_selected_node(&mut selected, group_name) {
|
||||||
return Ok((profiles, false));
|
return Ok((profiles, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
item.selected = (!selected.is_empty()).then_some(selected);
|
item.selected = (!selected.is_empty()).then_some(selected);
|
||||||
profiles.save_file().await?;
|
profiles.save_file().await?;
|
||||||
Ok((profiles, true))
|
Ok((profiles, true))
|
||||||
})
|
})
|
||||||
.await?;
|
.await
|
||||||
|
|
||||||
if cleared {
|
|
||||||
supersede_selected_activation();
|
|
||||||
handle::Handle::refresh_profiles();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn persist_reconciled_selected(
|
async fn persist_reconciled_selected(
|
||||||
@@ -781,34 +794,27 @@ async fn persist_reconciled_selected(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let profiles = Config::profiles().await;
|
|
||||||
let profile_uid = profile_uid.clone();
|
let profile_uid = profile_uid.clone();
|
||||||
let original_selected = original_selected.to_vec();
|
let original_selected = original_selected.to_vec();
|
||||||
let updated = profiles
|
persist_selected_update(false, move |mut profiles| async move {
|
||||||
.with_data_modify(move |mut profiles| async move {
|
if !is_activation_current(generation) || profiles.current.as_ref() != Some(&profile_uid) {
|
||||||
if !is_activation_current(generation) || profiles.current.as_ref() != Some(&profile_uid) {
|
return Ok((profiles, false));
|
||||||
return Ok((profiles, false));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let profile = profiles
|
let profile = profiles
|
||||||
.items
|
.items
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(&profile_uid)))
|
.and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(&profile_uid)))
|
||||||
.with_context(|| format!("failed to find the profile item \"uid:{profile_uid}\""))?;
|
.with_context(|| format!("failed to find the profile item \"uid:{profile_uid}\""))?;
|
||||||
if profile.selected.as_deref().unwrap_or(&[]) != original_selected.as_slice() {
|
if profile.selected.as_deref().unwrap_or(&[]) != original_selected.as_slice() {
|
||||||
return Ok((profiles, false));
|
return Ok((profiles, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
profile.selected = (!selected.is_empty()).then_some(selected);
|
profile.selected = (!selected.is_empty()).then_some(selected);
|
||||||
profiles.save_file().await?;
|
profiles.save_file().await?;
|
||||||
Ok((profiles, true))
|
Ok((profiles, true))
|
||||||
})
|
})
|
||||||
.await?;
|
.await
|
||||||
|
|
||||||
if updated {
|
|
||||||
handle::Handle::refresh_profiles();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns recorded selections whose groups have not reached the requested node.
|
/// Returns recorded selections whose groups have not reached the requested node.
|
||||||
@@ -830,48 +836,58 @@ fn unsettled_selections(selected: &[PrfSelected], proxies: &Proxies) -> Vec<Stri
|
|||||||
/// Retries selections while provider-backed groups finish loading.
|
/// Retries selections while provider-backed groups finish loading.
|
||||||
async fn settle_pending_selections(selected: &[PrfSelected], completed: &mut HashMap<String, String>, generation: u64) {
|
async fn settle_pending_selections(selected: &[PrfSelected], completed: &mut HashMap<String, String>, generation: u64) {
|
||||||
let deadline = Instant::now() + SELECTED_NODES_SETTLE_DEADLINE;
|
let deadline = Instant::now() + SELECTED_NODES_SETTLE_DEADLINE;
|
||||||
loop {
|
let attempts = NonZeroUsize::MIN.saturating_add(
|
||||||
tokio::time::sleep(SELECTED_NODES_SETTLE_INTERVAL).await;
|
(SELECTED_NODES_SETTLE_DEADLINE.as_millis() / SELECTED_NODES_SETTLE_INTERVAL.as_millis()) as usize,
|
||||||
if !is_activation_current(generation) {
|
);
|
||||||
return;
|
tokio::time::sleep(SELECTED_NODES_SETTLE_INTERVAL).await;
|
||||||
}
|
let _ = retry_with_state(
|
||||||
let Ok(snapshot) = fetch_proxies_with_timeout().await else {
|
RetryPolicy::fixed(attempts, SELECTED_NODES_SETTLE_INTERVAL),
|
||||||
// Unreachable core: the deadline still applies, so this cannot spin forever.
|
(selected, completed, generation, deadline),
|
||||||
if Instant::now() >= deadline {
|
|state, _| {
|
||||||
return;
|
Box::pin(async move {
|
||||||
}
|
let (selected, completed, generation, deadline) = state;
|
||||||
continue;
|
if !is_activation_current(*generation) {
|
||||||
};
|
return Ok(());
|
||||||
if !is_activation_current(generation) {
|
}
|
||||||
return;
|
let Ok(snapshot) = fetch_proxies_with_timeout().await else {
|
||||||
}
|
return if Instant::now() >= *deadline {
|
||||||
|
Ok(())
|
||||||
let pending = unsettled_selections(selected, &snapshot);
|
} else {
|
||||||
if pending.is_empty() {
|
Err(RetryError::Retry(()))
|
||||||
return;
|
};
|
||||||
}
|
};
|
||||||
if Instant::now() >= deadline {
|
if !is_activation_current(*generation) {
|
||||||
logging!(
|
return Ok(());
|
||||||
warn,
|
}
|
||||||
Type::Config,
|
let pending = unsettled_selections(selected, &snapshot);
|
||||||
"gave up putting back {} selected node(s) the core never loaded: {}",
|
if pending.is_empty() {
|
||||||
pending.len(),
|
return Ok(());
|
||||||
pending.iter().map(String::as_str).collect::<Vec<_>>().join(", ")
|
}
|
||||||
);
|
if Instant::now() >= *deadline {
|
||||||
return;
|
logging!(
|
||||||
}
|
warn,
|
||||||
|
Type::Config,
|
||||||
let plan = reconcile_selected_nodes(selected, None, &snapshot);
|
"gave up putting back {} selected node(s) the core never loaded: {}",
|
||||||
if apply_activations(&plan.activations, completed, generation)
|
pending.len(),
|
||||||
.await
|
pending.iter().map(String::as_str).collect::<Vec<_>>().join(", ")
|
||||||
.is_none()
|
);
|
||||||
{
|
return Ok(());
|
||||||
return;
|
}
|
||||||
}
|
let plan = reconcile_selected_nodes(selected, None, &snapshot);
|
||||||
if is_activation_current(generation) {
|
if apply_activations(&plan.activations, completed, *generation)
|
||||||
handle::Handle::refresh_clash();
|
.await
|
||||||
}
|
.is_none()
|
||||||
}
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if is_activation_current(*generation) {
|
||||||
|
announce(Refresh::Clash);
|
||||||
|
}
|
||||||
|
Err(RetryError::Retry(()))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Releases the first-pass waiter even when restoration exits early.
|
/// Releases the first-pass waiter even when restoration exits early.
|
||||||
@@ -914,7 +930,7 @@ async fn activate_selected_nodes_worker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_activation_current(generation) {
|
if is_activation_current(generation) {
|
||||||
handle::Handle::refresh_clash();
|
announce(Refresh::Clash);
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = if needs_confirmation {
|
let plan = if needs_confirmation {
|
||||||
@@ -938,7 +954,7 @@ async fn activate_selected_nodes_worker(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if confirmed_activated_count > 0 && is_activation_current(generation) {
|
if confirmed_activated_count > 0 && is_activation_current(generation) {
|
||||||
handle::Handle::refresh_clash();
|
announce(Refresh::Clash);
|
||||||
}
|
}
|
||||||
confirmed_plan
|
confirmed_plan
|
||||||
} else {
|
} else {
|
||||||
@@ -1028,7 +1044,7 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
|
|||||||
|
|
||||||
if selected.is_empty() {
|
if selected.is_empty() {
|
||||||
if is_activation_current(generation) {
|
if is_activation_current(generation) {
|
||||||
handle::Handle::refresh_clash();
|
announce(Refresh::Clash);
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -1040,7 +1056,7 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot
|
|||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
logging!(error, Type::Config, "failed to activate selected nodes: {err:#}");
|
logging!(error, Type::Config, "failed to activate selected nodes: {err:#}");
|
||||||
// The profile itself is already active even if node restoration failed.
|
// The profile itself is already active even if node restoration failed.
|
||||||
handle::Handle::refresh_clash();
|
announce(Refresh::Clash);
|
||||||
}
|
}
|
||||||
update_tray_after_activation(generation).await;
|
update_tray_after_activation(generation).await;
|
||||||
logging!(debug, Type::Config, "activating selected nodes done!");
|
logging!(debug, Type::Config, "activating selected nodes done!");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, bail};
|
|||||||
use std::{io::ErrorKind, path::PathBuf};
|
use std::{io::ErrorKind, path::PathBuf};
|
||||||
|
|
||||||
pub(crate) struct FileSnapshot {
|
pub(crate) struct FileSnapshot {
|
||||||
path: PathBuf,
|
pub(crate) path: PathBuf,
|
||||||
content: Option<Vec<u8>>,
|
content: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,26 +11,30 @@ use serde::{Deserialize, Serialize};
|
|||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
|
|
||||||
/// ### `verge.yaml` schema
|
/// ### `verge.yaml` schema
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct IVerge {
|
pub struct IVerge {
|
||||||
/// silent | error | warn | info | debug | trace
|
/// silent | error | warn | info | debug | trace
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub app_log_level: Option<String>,
|
pub app_log_level: Option<String>,
|
||||||
|
|
||||||
/// Maximum app log size in KiB.
|
/// Maximum app log size in KiB.
|
||||||
pub app_log_max_size: Option<u64>,
|
pub app_log_max_size: Option<u64>,
|
||||||
|
|
||||||
pub app_log_max_count: Option<usize>,
|
pub app_log_max_count: Option<usize>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub language: Option<String>,
|
pub language: Option<String>,
|
||||||
|
|
||||||
/// `light` or `dark` or `system`
|
/// `light` or `dark` or `system`
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub theme_mode: Option<String>,
|
pub theme_mode: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub tray_event: Option<String>,
|
pub tray_event: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub env_type: Option<String>,
|
pub env_type: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub start_page: Option<String>,
|
pub start_page: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub startup_script: Option<String>,
|
pub startup_script: Option<String>,
|
||||||
|
|
||||||
pub traffic_graph: Option<bool>,
|
pub traffic_graph: Option<bool>,
|
||||||
@@ -48,18 +52,23 @@ pub struct IVerge {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub tray_icon: Option<String>,
|
pub tray_icon: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub menu_icon: Option<String>,
|
pub menu_icon: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<Vec<std::string::String>>)]
|
||||||
pub menu_order: Option<Vec<String>>,
|
pub menu_order: Option<Vec<String>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub proxy_group_tools_position: Option<String>,
|
pub proxy_group_tools_position: Option<String>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub notice_position: Option<String>,
|
pub notice_position: Option<String>,
|
||||||
|
|
||||||
pub collapse_navbar: Option<bool>,
|
pub collapse_navbar: Option<bool>,
|
||||||
@@ -80,44 +89,47 @@ pub struct IVerge {
|
|||||||
|
|
||||||
pub enable_bypass_check: Option<bool>,
|
pub enable_bypass_check: Option<bool>,
|
||||||
|
|
||||||
/// enable dns settings - this controls whether dns_config.yaml is applied
|
/// Initial DNS override preference for profiles without a saved setting.
|
||||||
pub enable_dns_settings: Option<bool>,
|
pub enable_dns_settings: Option<bool>,
|
||||||
|
|
||||||
// Force-enable confirmation is valid only for the current app session.
|
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||||
#[serde(skip)]
|
#[specta(type = std::collections::BTreeMap<std::string::String, super::dns::ProfileDnsSettings>)]
|
||||||
pub dns_override_confirmation: Option<String>,
|
pub profile_dns_settings: std::collections::BTreeMap<String, super::dns::ProfileDnsSettings>,
|
||||||
|
|
||||||
pub use_default_bypass: Option<bool>,
|
pub use_default_bypass: Option<bool>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub system_proxy_bypass: Option<String>,
|
pub system_proxy_bypass: Option<String>,
|
||||||
|
|
||||||
pub proxy_guard_duration: Option<u64>,
|
pub proxy_guard_duration: Option<u64>,
|
||||||
|
|
||||||
pub proxy_auto_config: Option<bool>,
|
pub proxy_auto_config: Option<bool>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub pac_file_content: Option<String>,
|
pub pac_file_content: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub proxy_host: Option<String>,
|
pub proxy_host: Option<String>,
|
||||||
|
|
||||||
pub theme_setting: Option<IVergeTheme>,
|
pub theme_setting: Option<IVergeTheme>,
|
||||||
|
#[specta(type = Option<Vec<std::string::String>>)]
|
||||||
pub web_ui_list: Option<Vec<String>>,
|
pub web_ui_list: Option<Vec<String>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub clash_core: Option<String>,
|
pub clash_core: Option<String>,
|
||||||
|
|
||||||
/// format: {func},{key}
|
/// format: {func},{key}
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<Vec<std::string::String>>)]
|
||||||
pub hotkeys: Option<Vec<String>>,
|
pub hotkeys: Option<Vec<String>>,
|
||||||
|
|
||||||
pub enable_global_hotkey: Option<bool>,
|
pub enable_global_hotkey: Option<bool>,
|
||||||
|
|
||||||
|
#[specta(type = Option<crate::ipc::JsonValue>)]
|
||||||
pub home_cards: Option<serde_json::Value>,
|
pub home_cards: Option<serde_json::Value>,
|
||||||
|
|
||||||
pub auto_close_connection: Option<bool>,
|
pub auto_close_connection: Option<bool>,
|
||||||
|
|
||||||
pub auto_check_update: Option<bool>,
|
pub auto_check_update: Option<bool>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub default_latency_test: Option<String>,
|
pub default_latency_test: Option<String>,
|
||||||
|
|
||||||
pub default_latency_timeout: Option<i16>,
|
pub default_latency_timeout: Option<i16>,
|
||||||
@@ -172,6 +184,7 @@ pub struct IVerge {
|
|||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
default
|
default
|
||||||
)]
|
)]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub webdav_url: Option<String>,
|
pub webdav_url: Option<String>,
|
||||||
|
|
||||||
#[serde(
|
#[serde(
|
||||||
@@ -180,6 +193,7 @@ pub struct IVerge {
|
|||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
default
|
default
|
||||||
)]
|
)]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub webdav_username: Option<String>,
|
pub webdav_username: Option<String>,
|
||||||
|
|
||||||
#[serde(
|
#[serde(
|
||||||
@@ -188,12 +202,14 @@ pub struct IVerge {
|
|||||||
skip_serializing_if = "Option::is_none",
|
skip_serializing_if = "Option::is_none",
|
||||||
default
|
default
|
||||||
)]
|
)]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub webdav_password: Option<String>,
|
pub webdav_password: Option<String>,
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub enable_tray_speed: Option<bool>,
|
pub enable_tray_speed: Option<bool>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub tray_proxy_groups_display_mode: Option<String>,
|
pub tray_proxy_groups_display_mode: Option<String>,
|
||||||
pub tray_inline_outbound_modes: Option<bool>,
|
pub tray_inline_outbound_modes: Option<bool>,
|
||||||
|
|
||||||
@@ -210,27 +226,41 @@ pub struct IVerge {
|
|||||||
pub enable_external_controller: Option<bool>,
|
pub enable_external_controller: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct IVergeTestItem {
|
pub struct IVergeTestItem {
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub uid: Option<String>,
|
pub uid: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
|
||||||
|
#[serde(default)]
|
||||||
pub struct IVergeTheme {
|
pub struct IVergeTheme {
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub primary_color: Option<String>,
|
pub primary_color: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub secondary_color: Option<String>,
|
pub secondary_color: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub primary_text: Option<String>,
|
pub primary_text: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub secondary_text: Option<String>,
|
pub secondary_text: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub info_color: Option<String>,
|
pub info_color: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub error_color: Option<String>,
|
pub error_color: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub warning_color: Option<String>,
|
pub warning_color: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub success_color: Option<String>,
|
pub success_color: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub font_family: Option<String>,
|
pub font_family: Option<String>,
|
||||||
|
#[specta(type = Option<std::string::String>)]
|
||||||
pub css_injection: Option<String>,
|
pub css_injection: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ pub mod files {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod tun {
|
pub mod tun {
|
||||||
pub const DEFAULT_STACK: &str = "gvisor";
|
pub const DEFAULT_STACK: &str = "mips";
|
||||||
|
|
||||||
pub const DNS_HIJACK: &[&str] = &["any:53"];
|
pub const DNS_HIJACK: &[&str] = &["any:53"];
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ use crate::{
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc, OnceLock,
|
||||||
atomic::{AtomicBool, AtomicI64, Ordering},
|
atomic::{AtomicBool, AtomicI64, Ordering},
|
||||||
},
|
},
|
||||||
time::{Duration, UNIX_EPOCH},
|
time::{Duration, UNIX_EPOCH},
|
||||||
@@ -92,7 +91,7 @@ pub struct AutoBackupManager {
|
|||||||
|
|
||||||
impl AutoBackupManager {
|
impl AutoBackupManager {
|
||||||
pub fn global() -> &'static Self {
|
pub fn global() -> &'static Self {
|
||||||
static INSTANCE: OnceCell<AutoBackupManager> = OnceCell::new();
|
static INSTANCE: OnceLock<AutoBackupManager> = OnceLock::new();
|
||||||
INSTANCE.get_or_init(|| {
|
INSTANCE.get_or_init(|| {
|
||||||
let (tx, _rx) = watch::channel(AutoBackupSettings::default());
|
let (tx, _rx) = watch::channel(AutoBackupSettings::default());
|
||||||
Self {
|
Self {
|
||||||
@@ -24,7 +24,7 @@ pub async fn update_launch() -> Result<()> {
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
{
|
{
|
||||||
let app_handle = Handle::app_handle();
|
let app_handle = Handle::app_handle();
|
||||||
let autostart_manager = app_handle.autolaunch();
|
let autostart_manager = app_handle.autostart();
|
||||||
if is_enable {
|
if is_enable {
|
||||||
logging_error!(Type::System, "{:?}", autostart_manager.enable());
|
logging_error!(Type::System, "{:?}", autostart_manager.enable());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use anyhow::Error;
|
|||||||
use arc_swap::{ArcSwap, ArcSwapOption};
|
use arc_swap::{ArcSwap, ArcSwapOption};
|
||||||
use backon::{ConstantBuilder, Retryable as _};
|
use backon::{ConstantBuilder, Retryable as _};
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
|
use reqwest_dav::list_cmd::{ListEntity, ListFile, ListMultiStatus};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -12,7 +11,7 @@ use std::{
|
|||||||
env::{consts::OS, temp_dir},
|
env::{consts::OS, temp_dir},
|
||||||
io::Write as _,
|
io::Write as _,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::Arc,
|
sync::{Arc, OnceLock},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::{fs, time::timeout};
|
use tokio::{fs, time::timeout};
|
||||||
@@ -58,7 +57,7 @@ pub struct WebDavClient {
|
|||||||
|
|
||||||
impl WebDavClient {
|
impl WebDavClient {
|
||||||
pub fn global() -> &'static Self {
|
pub fn global() -> &'static Self {
|
||||||
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
|
static WEBDAV_CLIENT: OnceLock<WebDavClient> = OnceLock::new();
|
||||||
WEBDAV_CLIENT.get_or_init(|| Self {
|
WEBDAV_CLIENT.get_or_init(|| Self {
|
||||||
config: ArcSwapOption::new(None),
|
config: ArcSwapOption::new(None),
|
||||||
clients: ArcSwap::new(Arc::new(HashMap::new())),
|
clients: ArcSwap::new(Arc::new(HashMap::new())),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user