mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-12 05:32:09 +08:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3be5a27f | ||
|
|
4adac9e4b6 | ||
|
|
2c60f55f45 | ||
|
|
b8c13c0370 | ||
|
|
f86db4b344 | ||
|
|
389dddf512 | ||
|
|
304a65fc36 |
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:2": {
|
"ghcr.io/devcontainers/features/node:2": {
|
||||||
"version": "26.13.0",
|
"version": "26.13.1",
|
||||||
"pnpmVersion": "12.18.0"
|
"pnpmVersion": "12.18.0"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/rust:1": {
|
"ghcr.io/devcontainers/features/rust:1": {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
"version": "v9.0.0",
|
"version": "v9.0.0",
|
||||||
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
|
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
|
||||||
},
|
},
|
||||||
"github/gh-aw-actions/setup@v1.0.1": {
|
"github/gh-aw-actions/setup@v1.2.2": {
|
||||||
"repo": "github/gh-aw-actions/setup",
|
"repo": "github/gh-aw-actions/setup",
|
||||||
"version": "v1.0.1",
|
"version": "v1.2.2",
|
||||||
"sha": "221ba4f4319de6b8e809d1a15528b3ae6a818aa5"
|
"sha": "53d0749e19424a918acd82b4d140264ce48f2f14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"containers": {
|
"containers": {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -262,7 +262,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -395,7 +395,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -479,7 +479,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6.1.1
|
- uses: pnpm/action-setup@v6.1.1
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ 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@406c7719e31d7fe354224d4bb8fe3d4aeb47da1c # v1.2.1
|
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
|
||||||
with:
|
with:
|
||||||
version: v1.0.1
|
version: v1.2.2
|
||||||
- name: Install Rust Stable
|
- name: Install Rust Stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
if: github.event.inputs[matrix.input] == 'true'
|
if: github.event.inputs[matrix.input] == 'true'
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|||||||
+282
-103
File diff suppressed because one or more lines are too long
@@ -33,6 +33,7 @@ max-daily-ai-credits: 5000
|
|||||||
permissions:
|
permissions:
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
copilot-requests: none
|
||||||
|
|
||||||
pre-agent-steps:
|
pre-agent-steps:
|
||||||
- name: Make Copilot quota exhaustion non-blocking
|
- name: Make Copilot quota exhaustion non-blocking
|
||||||
|
|||||||
Generated
+288
-107
File diff suppressed because one or more lines are too long
@@ -30,6 +30,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
copilot-requests: none
|
||||||
steps:
|
steps:
|
||||||
- name: Load review rules from the base revision
|
- name: Load review rules from the base revision
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
if: github.event.action != 'closed'
|
if: github.event.action != 'closed'
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
- name: Validate source and publish the approved snapshot
|
- name: Validate source and publish the approved snapshot
|
||||||
id: source
|
id: source
|
||||||
env:
|
env:
|
||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -330,7 +330,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -461,7 +461,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Pnpm install and check
|
- name: Pnpm install and check
|
||||||
@@ -542,7 +542,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
with:
|
with:
|
||||||
run_install: false
|
run_install: false
|
||||||
@@ -565,7 +565,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -593,7 +593,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -661,7 +661,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
+282
-103
File diff suppressed because one or more lines are too long
@@ -28,6 +28,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
issues: read
|
issues: read
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
copilot-requests: none
|
||||||
max-ai-credits: 60
|
max-ai-credits: 60
|
||||||
max-turns: 12
|
max-turns: 12
|
||||||
max-daily-ai-credits: 500
|
max-daily-ai-credits: 500
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v6
|
uses: pnpm/action-setup@v6
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '26.13.0'
|
node-version: '26.13.1'
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
nodejs 26.13.0
|
nodejs 26.13.1
|
||||||
pnpm 12.18.0
|
pnpm 12.18.0
|
||||||
rust 1.99.1
|
rust 1.99.1
|
||||||
|
|||||||
Generated
+4
-4
@@ -159,9 +159,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
version = "1.9.4"
|
version = "1.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "db4c94f57a570fce5d381be3289b3f79aef6332d72948fe82ab5772e423ccf92"
|
checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
@@ -10126,9 +10126,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "9.4.2"
|
version = "9.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3845f734baabdd02de353a20580e30ceccd03897def44a4eb8e9654992e294"
|
checksum = "0700e6c427aff64acdbf02e85fbfd946c2ee12319d1af013092f3a2178914ff8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
|
|||||||
+3
-1
@@ -24,12 +24,13 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong> 🐞 修复问题 </strong></summary>
|
<summary><strong> 🐞 修复问题 </strong></summary>
|
||||||
|
|
||||||
|
- 修复应用退出时无条件重写 `clash.yaml`、`verge.yaml` 和 `profiles.yaml` 导致文件创建时间丢失的问题
|
||||||
|
|
||||||
- 修复检测更新后又发布新版时,应用内更新失败
|
- 修复检测更新后又发布新版时,应用内更新失败
|
||||||
- 修复订阅响应过大导致内存占用过高,限制单次下载为 4 MiB
|
- 修复订阅响应过大导致内存占用过高,限制单次下载为 4 MiB
|
||||||
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
|
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
|
||||||
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
|
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
|
||||||
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
|
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
|
||||||
- 修复切换订阅后节点延迟显示旧订阅结果,进入代理页面时主动刷新延迟记录
|
|
||||||
|
|
||||||
**🍎 macOS**
|
**🍎 macOS**
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
|
- 新增连接页面按网络协议(TCP/UDP)筛选连接
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -74,12 +74,16 @@ impl<T: Clone> Draft<T> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commits and clears the draft.
|
/// Commits and clears the draft, reporting whether one was staged.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn apply(&self) {
|
pub fn apply(&self) -> bool {
|
||||||
let mut guard = self.inner.data.lock();
|
let mut guard = self.inner.data.lock();
|
||||||
if let Some(d) = guard.1.take() {
|
match guard.1.take() {
|
||||||
guard.0 = d;
|
Some(d) => {
|
||||||
|
guard.0 = d;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -67,7 +67,7 @@
|
|||||||
"dayjs": "1.11.23",
|
"dayjs": "1.11.23",
|
||||||
"foxact": "^0.3.12",
|
"foxact": "^0.3.12",
|
||||||
"foxts": "^5.9.1",
|
"foxts": "^5.9.1",
|
||||||
"i18next": "^26.4.2",
|
"i18next": "^27.0.0",
|
||||||
"js-yaml": "^5.4.2",
|
"js-yaml": "^5.4.2",
|
||||||
"lodash-es": "^4.18.1",
|
"lodash-es": "^4.18.1",
|
||||||
"meta-json-schema": "^1.19.31",
|
"meta-json-schema": "^1.19.31",
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
"react-dom": "19.3.1",
|
"react-dom": "19.3.1",
|
||||||
"react-error-boundary": "6.1.6",
|
"react-error-boundary": "6.1.6",
|
||||||
"react-hook-form": "^7.88.0",
|
"react-hook-form": "^7.88.0",
|
||||||
"react-i18next": "17.1.13",
|
"react-i18next": "18.0.1",
|
||||||
"react-markdown": "10.1.0",
|
"react-markdown": "10.1.0",
|
||||||
"react-router": "^8.4.0",
|
"react-router": "^8.4.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
|||||||
Generated
+18
-21
@@ -239,8 +239,8 @@ importers:
|
|||||||
specifier: ^5.9.1
|
specifier: ^5.9.1
|
||||||
version: 5.9.1
|
version: 5.9.1
|
||||||
i18next:
|
i18next:
|
||||||
specifier: ^26.4.2
|
specifier: ^27.0.0
|
||||||
version: 26.4.2(typescript@6.0.3)
|
version: 27.0.1(typescript@6.0.3)
|
||||||
js-yaml:
|
js-yaml:
|
||||||
specifier: ^5.4.2
|
specifier: ^5.4.2
|
||||||
version: 5.4.2
|
version: 5.4.2
|
||||||
@@ -275,8 +275,8 @@ importers:
|
|||||||
specifier: ^7.88.0
|
specifier: ^7.88.0
|
||||||
version: 7.88.0(react@19.3.1)
|
version: 7.88.0(react@19.3.1)
|
||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: 17.1.13
|
specifier: 18.0.1
|
||||||
version: 17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
|
version: 18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3)
|
||||||
react-markdown:
|
react-markdown:
|
||||||
specifier: 10.1.0
|
specifier: 10.1.0
|
||||||
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
|
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
|
||||||
@@ -3689,8 +3689,9 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
i18next@26.4.2:
|
i18next@27.0.1:
|
||||||
resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
|
resolution: {integrity: sha512-KaE5ozP8UUeVpOJIwMVbbnpRKpyYQuxd9ZbseJoDzezE3nvLnZZQn20pkzyIJwPI2Uet/Qs3WzPTG812f6F/Eg==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^5 || ^6 || ^7
|
typescript: ^5 || ^6 || ^7
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@@ -4396,18 +4397,16 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
react-i18next@17.1.13:
|
react-i18next@18.0.1:
|
||||||
resolution: {integrity: sha512-ZxhOSlyKSKPtbUNPLwYMmE3w3bhoc0jaZJZ6bc7wGOT6cLbrbs//6m01HxCLtFdSjHvFpwFwoPhxxzIlHSs9cw==}
|
resolution: {integrity: sha512-kPWXI0E/lrPj3+/umcKNzYZVnWmEedLiamMzDIHfbs+5ANf6yzvlZsjKou8f7kaGkfMMDLIZBejV+4aSN1CyBw==}
|
||||||
|
engines: {node: '>=22'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
i18next: '>= 26.2.0'
|
'@types/react': '*'
|
||||||
react: '>= 16.8.0'
|
i18next: '>= 27.0.1'
|
||||||
react-dom: '*'
|
react: '>= 18.0.0'
|
||||||
react-native: '*'
|
|
||||||
typescript: ^5 || ^6 || ^7
|
typescript: ^5 || ^6 || ^7
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
react-dom:
|
'@types/react':
|
||||||
optional: true
|
|
||||||
react-native:
|
|
||||||
optional: true
|
optional: true
|
||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
@@ -8533,7 +8532,7 @@ snapshots:
|
|||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
i18next@26.4.2(typescript@6.0.3):
|
i18next@27.0.1(typescript@6.0.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
@@ -9357,15 +9356,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.3.1
|
react: 19.3.1
|
||||||
|
|
||||||
react-i18next@17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
|
react-i18next@18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.29.7
|
|
||||||
html-parse-stringify: 4.0.1
|
html-parse-stringify: 4.0.1
|
||||||
i18next: 26.4.2(typescript@6.0.3)
|
i18next: 27.0.1(typescript@6.0.3)
|
||||||
react: 19.3.1
|
react: 19.3.1
|
||||||
use-sync-external-store: 1.7.0(react@19.3.1)
|
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react-dom: 19.3.1(react@19.3.1)
|
'@types/react': 19.3.0
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ tauri-plugin-dialog = "2.8.4"
|
|||||||
tauri-plugin-fs = "2.7.1"
|
tauri-plugin-fs = "2.7.1"
|
||||||
tauri-plugin-deep-link = "2.6.3"
|
tauri-plugin-deep-link = "2.6.3"
|
||||||
tauri-plugin-window-state = "2.5.0"
|
tauri-plugin-window-state = "2.5.0"
|
||||||
zip = "9.4.2"
|
zip = "9.5.0"
|
||||||
flate2 = "1.1.11"
|
flate2 = "1.1.11"
|
||||||
reqwest_dav = "0.3.3"
|
reqwest_dav = "0.3.3"
|
||||||
aes-gcm = { version = "0.11.1" }
|
aes-gcm = { version = "0.11.1" }
|
||||||
@@ -97,7 +97,7 @@ async-trait = "0.1.92"
|
|||||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
|
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
|
||||||
"client",
|
"client",
|
||||||
] }
|
] }
|
||||||
arc-swap = "1.9.4"
|
arc-swap = "1.9.5"
|
||||||
tokio-rustls = "0.26.6"
|
tokio-rustls = "0.26.6"
|
||||||
rustls = { version = "0.23.46", features = ["ring"] }
|
rustls = { version = "0.23.46", features = ["ring"] }
|
||||||
webpki-roots = "1.0.11"
|
webpki-roots = "1.0.11"
|
||||||
|
|||||||
@@ -280,25 +280,29 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Commits drafts during exit/restart/shutdown so user changes are not lost.
|
/// Commits drafts during exit/restart/shutdown so user changes are not lost.
|
||||||
|
/// A config with nothing staged keeps its file, so its timestamps stay put.
|
||||||
pub async fn apply_all_and_save_file() {
|
pub async fn apply_all_and_save_file() {
|
||||||
logging!(debug, Type::Config, "save all draft data");
|
logging!(debug, Type::Config, "save all draft data");
|
||||||
let save_clash_task = AsyncHandler::spawn(|| async {
|
let save_clash_task = AsyncHandler::spawn(|| async {
|
||||||
let clash = Self::clash().await;
|
let clash = Self::clash().await;
|
||||||
clash.apply();
|
if clash.apply() {
|
||||||
logging_error!(Type::Config, clash.data_arc().save_config().await);
|
logging_error!(Type::Config, clash.data_arc().save_config().await);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let save_verge_task = AsyncHandler::spawn(|| async {
|
let save_verge_task = AsyncHandler::spawn(|| async {
|
||||||
let verge = Self::verge().await;
|
let verge = Self::verge().await;
|
||||||
verge.apply();
|
if verge.apply() {
|
||||||
logging_error!(Type::Config, verge.data_arc().save_file().await);
|
logging_error!(Type::Config, verge.data_arc().save_file().await);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let save_profiles_task = AsyncHandler::spawn(|| async {
|
let save_profiles_task = AsyncHandler::spawn(|| async {
|
||||||
let _profile_write = super::profiles::PROFILE_WRITE_LOCK.lock().await;
|
let _profile_write = super::profiles::PROFILE_WRITE_LOCK.lock().await;
|
||||||
let profiles = Self::profiles().await;
|
let profiles = Self::profiles().await;
|
||||||
profiles.apply();
|
if profiles.apply() {
|
||||||
logging_error!(Type::Config, profiles.data_arc().save_file().await);
|
logging_error!(Type::Config, profiles.data_arc().save_file().await);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let _ = tokio::join!(save_clash_task, save_verge_task, save_profiles_task);
|
let _ = tokio::join!(save_clash_task, save_verge_task, save_profiles_task);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useLockFn } from 'ahooks'
|
import { useLockFn } from 'ahooks'
|
||||||
import { useCallback, useSyncExternalStore } from 'react'
|
import { useCallback, useEffect, useReducer } from 'react'
|
||||||
|
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import delayManager, { type DelayUpdate } from '@/services/delay'
|
import delayManager, { type DelayUpdate } from '@/services/delay'
|
||||||
@@ -17,6 +17,13 @@ const PRESET_PROXY_NAMES = [
|
|||||||
'COMPATIBLE',
|
'COMPATIBLE',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const identity = (previous: DelayUpdate, next: DelayUpdate): DelayUpdate =>
|
||||||
|
previous.delay === next.delay &&
|
||||||
|
previous.elapsed === next.elapsed &&
|
||||||
|
previous.updatedAt === next.updatedAt
|
||||||
|
? previous
|
||||||
|
: next
|
||||||
|
|
||||||
const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 }
|
const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 }
|
||||||
|
|
||||||
export interface UseProxyDelayState {
|
export interface UseProxyDelayState {
|
||||||
@@ -35,37 +42,56 @@ export function useProxyDelayState(
|
|||||||
const details = memberDetails(member)
|
const details = memberDetails(member)
|
||||||
const unresolved = member.kind === 'unresolved'
|
const unresolved = member.kind === 'unresolved'
|
||||||
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
||||||
|
const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY)
|
||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const timeout = verge?.default_latency_timeout || 10000
|
const timeout = verge?.default_latency_timeout || 10000
|
||||||
|
|
||||||
const subscribe = useCallback(
|
useEffect(() => {
|
||||||
(onChange: () => void) =>
|
if (isPreset) return
|
||||||
isPreset ? () => {} : delayManager.addListener(name, groupName, onChange),
|
delayManager.setListener(name, groupName, setDelayState)
|
||||||
[name, groupName, isPreset],
|
return () => {
|
||||||
)
|
delayManager.removeListener(name, groupName)
|
||||||
const read = useCallback(
|
}
|
||||||
() =>
|
}, [name, groupName, isPreset])
|
||||||
unresolved ? undefined : delayManager.getDelayUpdate(name, groupName),
|
|
||||||
[name, groupName, unresolved],
|
|
||||||
)
|
|
||||||
const cachedUpdate = useSyncExternalStore(subscribe, read, read)
|
|
||||||
|
|
||||||
const fallbackDelay = delayManager.getDelayFix(member, groupName)
|
const updateDelay = useCallback(() => {
|
||||||
const history = details?.history
|
if (unresolved) {
|
||||||
const lastRecord = history?.[history.length - 1]
|
setDelayState(INITIAL_DELAY)
|
||||||
const parsed = lastRecord ? Date.parse(lastRecord.time) : NaN
|
return
|
||||||
const delayState =
|
}
|
||||||
cachedUpdate ??
|
const cachedUpdate = delayManager.getDelayUpdate(name, groupName)
|
||||||
(fallbackDelay === -1
|
if (cachedUpdate) {
|
||||||
? INITIAL_DELAY
|
setDelayState({ ...cachedUpdate })
|
||||||
: {
|
return
|
||||||
delay: fallbackDelay,
|
}
|
||||||
updatedAt: Number.isNaN(parsed) ? 0 : parsed,
|
|
||||||
})
|
const fallbackDelay = delayManager.getDelayFix(member, groupName)
|
||||||
|
if (fallbackDelay === -1) {
|
||||||
|
setDelayState({ delay: -1, updatedAt: 0 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedAt = 0
|
||||||
|
const history = details?.history
|
||||||
|
if (history && history.length > 0) {
|
||||||
|
const lastRecord = history[history.length - 1]
|
||||||
|
const parsed = Date.parse(lastRecord.time)
|
||||||
|
if (!Number.isNaN(parsed)) {
|
||||||
|
updatedAt = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDelayState({ delay: fallbackDelay, updatedAt })
|
||||||
|
}, [details?.history, groupName, member, name, unresolved])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateDelay()
|
||||||
|
}, [updateDelay])
|
||||||
|
|
||||||
const onDelay = useLockFn(async () => {
|
const onDelay = useLockFn(async () => {
|
||||||
if (!isInteractableMember(member)) return
|
if (!isInteractableMember(member)) return
|
||||||
await delayManager.checkDelay(member, groupName, timeout)
|
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||||
|
setDelayState(await delayManager.checkDelay(member, groupName, timeout))
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ const ORDER_OPTIONS = [
|
|||||||
|
|
||||||
type OrderKey = (typeof ORDER_OPTIONS)[number]['id']
|
type OrderKey = (typeof ORDER_OPTIONS)[number]['id']
|
||||||
|
|
||||||
|
type NetworkFilter = 'all' | 'tcp' | 'udp'
|
||||||
|
|
||||||
|
let savedNetworkFilter: NetworkFilter = 'all'
|
||||||
|
|
||||||
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
|
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
|
||||||
(acc, option) => {
|
(acc, option) => {
|
||||||
acc[option.id] = option.fn
|
acc[option.id] = option.fn
|
||||||
@@ -86,6 +90,9 @@ const ConnectionsPage = () => {
|
|||||||
const { searchState, match, onSearch } = usePageSearch('connections')
|
const { searchState, match, onSearch } = usePageSearch('connections')
|
||||||
const hasSearch = searchState.text.length > 0
|
const hasSearch = searchState.text.length > 0
|
||||||
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
|
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
|
||||||
|
const [networkFilter, setNetworkFilter] =
|
||||||
|
useState<NetworkFilter>(savedNetworkFilter)
|
||||||
|
const hasFilter = networkFilter !== 'all'
|
||||||
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
|
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
|
||||||
'active',
|
'active',
|
||||||
)
|
)
|
||||||
@@ -112,18 +119,30 @@ const ConnectionsPage = () => {
|
|||||||
const filterConn = useMemo(() => {
|
const filterConn = useMemo(() => {
|
||||||
const orderFunc = orderFunctionMap[curOrderOpt]
|
const orderFunc = orderFunctionMap[curOrderOpt]
|
||||||
|
|
||||||
if (isTableLayout && !hasSearch) return selectedConnections
|
if (isTableLayout && !hasSearch && !hasFilter) return selectedConnections
|
||||||
if (!hasSearch) return orderFunc([...selectedConnections])
|
if (!hasSearch && !hasFilter) return orderFunc([...selectedConnections])
|
||||||
|
|
||||||
const matchConns = selectedConnections.filter((conn) => {
|
const matchConns = selectedConnections.filter((conn) => {
|
||||||
const { host, destinationIP, process } = conn.metadata
|
const { host, destinationIP, process, network } = conn.metadata
|
||||||
|
if (networkFilter !== 'all' && network !== networkFilter) return false
|
||||||
return (
|
return (
|
||||||
match(host || '') || match(destinationIP || '') || match(process || '')
|
!hasSearch ||
|
||||||
|
match(host || '') ||
|
||||||
|
match(destinationIP || '') ||
|
||||||
|
match(process || '')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
return orderFunc ? orderFunc(matchConns) : matchConns
|
return orderFunc ? orderFunc(matchConns) : matchConns
|
||||||
}, [selectedConnections, isTableLayout, hasSearch, match, curOrderOpt])
|
}, [
|
||||||
|
selectedConnections,
|
||||||
|
isTableLayout,
|
||||||
|
hasSearch,
|
||||||
|
hasFilter,
|
||||||
|
networkFilter,
|
||||||
|
match,
|
||||||
|
curOrderOpt,
|
||||||
|
])
|
||||||
|
|
||||||
const displayRows = useConnectionRowViews(
|
const displayRows = useConnectionRowViews(
|
||||||
isTableLayout ? EMPTY_CONNECTIONS : filterConn,
|
isTableLayout ? EMPTY_CONNECTIONS : filterConn,
|
||||||
@@ -255,6 +274,24 @@ const ConnectionsPage = () => {
|
|||||||
))}
|
))}
|
||||||
</BaseStyledSelect>
|
</BaseStyledSelect>
|
||||||
)}
|
)}
|
||||||
|
<BaseStyledSelect
|
||||||
|
style={{ width: 80 }}
|
||||||
|
value={networkFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
savedNetworkFilter = e.target.value as NetworkFilter
|
||||||
|
setNetworkFilter(savedNetworkFilter)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="all">
|
||||||
|
<span style={{ fontSize: 14 }}>All</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="tcp">
|
||||||
|
<span style={{ fontSize: 14 }}>TCP</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value="udp">
|
||||||
|
<span style={{ fontSize: 14 }}>UDP</span>
|
||||||
|
</MenuItem>
|
||||||
|
</BaseStyledSelect>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -49,11 +49,7 @@ const ProxyPage = () => {
|
|||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
const { clashConfig } = useClashConfigData()
|
const { clashConfig } = useClashConfigData()
|
||||||
const { refreshClashConfig, refreshProxy } = useAppRefreshers()
|
const { refreshClashConfig } = useAppRefreshers()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refreshProxy().catch((error) => showNotice.error(error))
|
|
||||||
}, [refreshProxy])
|
|
||||||
|
|
||||||
const normalizedMode = clashConfig?.mode?.toLowerCase()
|
const normalizedMode = clashConfig?.mode?.toLowerCase()
|
||||||
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
|
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { useCallback, useMemo, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
getBaseConfig,
|
getBaseConfig,
|
||||||
getRuleProviders,
|
getRuleProviders,
|
||||||
@@ -6,10 +6,8 @@ import {
|
|||||||
} from 'tauri-plugin-mihomo-api'
|
} from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
import { useClashInfo, useRuntimeConfig } from '@/hooks/use-clash'
|
import { useClashInfo, useRuntimeConfig } from '@/hooks/use-clash'
|
||||||
import { useProfiles } from '@/hooks/use-profiles'
|
|
||||||
import { useVerge } from '@/hooks/use-verge'
|
import { useVerge } from '@/hooks/use-verge'
|
||||||
import { getProxyView, getSystemProxy } from '@/services/cmds'
|
import { getProxyView, getSystemProxy } from '@/services/cmds'
|
||||||
import delayManager from '@/services/delay'
|
|
||||||
import { useQuery } from '@/services/query-client'
|
import { useQuery } from '@/services/query-client'
|
||||||
import { useRunState } from '@/store/app-store-context'
|
import { useRunState } from '@/store/app-store-context'
|
||||||
import { resolveDisplayedMixedPort } from '@/utils/mixed-port'
|
import { resolveDisplayedMixedPort } from '@/utils/mixed-port'
|
||||||
@@ -55,8 +53,6 @@ export const AppDataProvider = ({
|
|||||||
const { verge } = useVerge()
|
const { verge } = useVerge()
|
||||||
const { data: runtimeConfig } = useRuntimeConfig()
|
const { data: runtimeConfig } = useRuntimeConfig()
|
||||||
const { clashInfo } = useClashInfo()
|
const { clashInfo } = useClashInfo()
|
||||||
const { profiles } = useProfiles()
|
|
||||||
const currentProfile = profiles?.current
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: proxyView,
|
data: proxyView,
|
||||||
@@ -106,12 +102,6 @@ export const AppDataProvider = ({
|
|||||||
const runningMode = runState?.mode
|
const runningMode = runState?.mode
|
||||||
|
|
||||||
const refreshProxy = useStableFn(_refetchProxyView)
|
const refreshProxy = useStableFn(_refetchProxyView)
|
||||||
useEffect(() => {
|
|
||||||
if (currentProfile === undefined) return
|
|
||||||
delayManager.setProfile(currentProfile)
|
|
||||||
void refreshProxy()
|
|
||||||
}, [currentProfile, refreshProxy])
|
|
||||||
|
|
||||||
const refreshClashConfig = useStableFn(_refetchClashConfig)
|
const refreshClashConfig = useStableFn(_refetchClashConfig)
|
||||||
const refreshRules = useStableFn(_refetchRules)
|
const refreshRules = useStableFn(_refetchRules)
|
||||||
const refreshSysproxy = useStableFn(_refetchSysproxy)
|
const refreshSysproxy = useStableFn(_refetchSysproxy)
|
||||||
|
|||||||
+16
-66
@@ -5,32 +5,26 @@ vi.mock('tauri-plugin-mihomo-api', () => ({
|
|||||||
healthcheckNodeInProvider: vi.fn(async () => ({ delay: 120 })),
|
healthcheckNodeInProvider: vi.fn(async () => ({ delay: 120 })),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
import type { InteractableProxyMember } from '@/types/proxy-view'
|
import type { ResolvedProxyMember } from '@/types/proxy-view'
|
||||||
|
|
||||||
import delayManager from './delay'
|
import delayManager from './delay'
|
||||||
|
|
||||||
const node = (
|
const node = (name: string) =>
|
||||||
name: string,
|
({
|
||||||
): Extract<InteractableProxyMember, { kind: 'node' }> => ({
|
kind: 'node',
|
||||||
kind: 'node',
|
ref: { kind: 'node', name, recordId: `r:${name}` },
|
||||||
ref: { kind: 'node', name, recordId: `r:${name}` },
|
node: {
|
||||||
node: {
|
recordId: `r:${name}`,
|
||||||
recordId: `r:${name}`,
|
name,
|
||||||
name,
|
history: [],
|
||||||
type: 'Shadowsocks',
|
source: { kind: 'core', proxyName: name },
|
||||||
history: [],
|
},
|
||||||
source: { kind: 'core', proxyName: name },
|
}) as unknown as ResolvedProxyMember
|
||||||
udp: false,
|
|
||||||
xudp: false,
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||||
tfo: false,
|
|
||||||
mptcp: false,
|
|
||||||
smux: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let settles = 0
|
let settles = 0
|
||||||
let unsubscribe: () => void
|
let unsubscribe: () => void
|
||||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
settles = 0
|
settles = 0
|
||||||
@@ -45,7 +39,7 @@ describe('group delay completion', () => {
|
|||||||
test('notifies once after a batch settles', async () => {
|
test('notifies once after a batch settles', async () => {
|
||||||
const proxies = Array.from({ length: 6 }, (_, index) => node(`n${index}`))
|
const proxies = Array.from({ length: 6 }, (_, index) => node(`n${index}`))
|
||||||
|
|
||||||
await delayManager.checkListDelay(proxies, 'g', 5000, 2)
|
await delayManager.checkListDelay(proxies as never, 'g', 5000, 2)
|
||||||
await flush()
|
await flush()
|
||||||
|
|
||||||
expect(settles).toBe(1)
|
expect(settles).toBe(1)
|
||||||
@@ -57,7 +51,7 @@ describe('group delay completion', () => {
|
|||||||
other += 1
|
other += 1
|
||||||
})
|
})
|
||||||
|
|
||||||
await delayManager.checkDelay(node('a'), 'g', 5000)
|
await delayManager.checkDelay(node('a') as never, 'g', 5000)
|
||||||
await flush()
|
await flush()
|
||||||
|
|
||||||
expect(settles).toBe(1)
|
expect(settles).toBe(1)
|
||||||
@@ -65,47 +59,3 @@ describe('group delay completion', () => {
|
|||||||
stop()
|
stop()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('profile delay isolation', () => {
|
|
||||||
test('notifies both shared-key occurrences and preserves the surviving subscription', async () => {
|
|
||||||
const first = vi.fn()
|
|
||||||
const second = vi.fn()
|
|
||||||
const stopFirst = delayManager.addListener('a', 'g', first)
|
|
||||||
const stopSecond = delayManager.addListener('a', 'g', second)
|
|
||||||
try {
|
|
||||||
delayManager.setDelay('a', 'g', -2)
|
|
||||||
const pending = delayManager.getDelayUpdate('a', 'g')
|
|
||||||
expect(delayManager.getDelayUpdate('a', 'g')).toBe(pending)
|
|
||||||
await flush()
|
|
||||||
stopFirst()
|
|
||||||
delayManager.setDelay('a', 'g', 120)
|
|
||||||
expect(delayManager.getDelayUpdate('a', 'g')).not.toBe(pending)
|
|
||||||
await flush()
|
|
||||||
expect(first.mock.calls.map(([update]) => update.delay)).toEqual([-2])
|
|
||||||
expect(second.mock.calls.map(([update]) => update.delay)).toEqual([
|
|
||||||
-2, 120,
|
|
||||||
])
|
|
||||||
} finally {
|
|
||||||
stopFirst()
|
|
||||||
stopSecond()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('clears cached delays and fences an old batch across a profile switch', async () => {
|
|
||||||
delayManager.setProfile('initial')
|
|
||||||
delayManager.setDelay('cached', 'g', 900)
|
|
||||||
const oldBatch = delayManager.checkListDelay(
|
|
||||||
[node('a'), node('not-started')],
|
|
||||||
'g',
|
|
||||||
5000,
|
|
||||||
1,
|
|
||||||
)
|
|
||||||
delayManager.setProfile('next')
|
|
||||||
expect(delayManager.getDelayUpdate('cached', 'g')).toBeUndefined()
|
|
||||||
delayManager.setDelay('a', 'g', 42)
|
|
||||||
await oldBatch
|
|
||||||
await flush()
|
|
||||||
expect(delayManager.getDelay('a', 'g')).toBe(42)
|
|
||||||
expect(delayManager.getDelayUpdate('not-started', 'g')).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|||||||
+33
-83
@@ -29,12 +29,10 @@ export interface DelayUpdate {
|
|||||||
const CACHE_TTL = 30 * 60 * 1000
|
const CACHE_TTL = 30 * 60 * 1000
|
||||||
|
|
||||||
class DelayManager {
|
class DelayManager {
|
||||||
private profile: string | null = null
|
|
||||||
private generation = 0
|
|
||||||
private cache = new Map<string, DelayUpdate>()
|
private cache = new Map<string, DelayUpdate>()
|
||||||
private urlMap = new Map<string, string>()
|
private urlMap = new Map<string, string>()
|
||||||
|
|
||||||
private listenerMap = new Map<string, Set<(update: DelayUpdate) => void>>()
|
private listenerMap = new Map<string, (update: DelayUpdate) => void>()
|
||||||
|
|
||||||
private groupListenerMap = new Map<string, Set<() => void>>()
|
private groupListenerMap = new Map<string, Set<() => void>>()
|
||||||
// Consumers compare snapshot identity; replace it only when the group settles.
|
// Consumers compare snapshot identity; replace it only when the group settles.
|
||||||
@@ -51,35 +49,6 @@ class DelayManager {
|
|||||||
private itemFlushScheduled = false
|
private itemFlushScheduled = false
|
||||||
private groupFlushScheduled = false
|
private groupFlushScheduled = false
|
||||||
|
|
||||||
setProfile(profile: string | null) {
|
|
||||||
if (this.profile === profile) return
|
|
||||||
this.profile = profile
|
|
||||||
this.generation += 1
|
|
||||||
this.cache.clear()
|
|
||||||
this.activeBatches.clear()
|
|
||||||
this.pendingItemUpdates.clear()
|
|
||||||
this.pendingGroupUpdates.clear()
|
|
||||||
this.groupSnapshots.clear()
|
|
||||||
this.groupSetSnapshots.clear()
|
|
||||||
|
|
||||||
// Reset before a refreshed proxy snapshot can publish newer history.
|
|
||||||
for (const [key, listeners] of this.listenerMap) {
|
|
||||||
for (const listener of [...listeners]) {
|
|
||||||
try {
|
|
||||||
listener({ delay: -1, updatedAt: 0 })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[DelayManager] Failed to reset delay listener: ${key}`,
|
|
||||||
error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const group of this.groupListenerMap.keys()) {
|
|
||||||
this.queueGroupNotification(group)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scheduleOnNextFrame(run: () => void): void {
|
private scheduleOnNextFrame(run: () => void): void {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
if (typeof window.requestAnimationFrame === 'function') {
|
if (typeof window.requestAnimationFrame === 'function') {
|
||||||
@@ -105,19 +74,17 @@ class DelayManager {
|
|||||||
this.pendingItemUpdates = new Map()
|
this.pendingItemUpdates = new Map()
|
||||||
|
|
||||||
updates.forEach((queue, key) => {
|
updates.forEach((queue, key) => {
|
||||||
const listeners = this.listenerMap.get(key)
|
const listener = this.listenerMap.get(key)
|
||||||
if (!listeners) return
|
if (!listener) return
|
||||||
|
|
||||||
queue.forEach((update) => {
|
queue.forEach((update) => {
|
||||||
for (const listener of [...listeners]) {
|
try {
|
||||||
try {
|
listener(update)
|
||||||
listener(update)
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error(
|
||||||
console.error(
|
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
||||||
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
error,
|
||||||
error,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -203,22 +170,18 @@ class DelayManager {
|
|||||||
return url || 'http://cp.cloudflare.com/generate_204'
|
return url || 'http://cp.cloudflare.com/generate_204'
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(
|
setListener(
|
||||||
name: string,
|
name: string,
|
||||||
group: string,
|
group: string,
|
||||||
listener: (update: DelayUpdate) => void,
|
listener: (update: DelayUpdate) => void,
|
||||||
): () => void {
|
) {
|
||||||
const key = hashKey(name, group)
|
const key = hashKey(name, group)
|
||||||
const listeners = this.listenerMap.get(key) ?? new Set()
|
this.listenerMap.set(key, listener)
|
||||||
listeners.add(listener)
|
}
|
||||||
this.listenerMap.set(key, listeners)
|
|
||||||
|
|
||||||
return () => {
|
removeListener(name: string, group: string) {
|
||||||
const current = this.listenerMap.get(key)
|
const key = hashKey(name, group)
|
||||||
if (!current) return
|
this.listenerMap.delete(key)
|
||||||
current.delete(listener)
|
|
||||||
if (current.size === 0) this.listenerMap.delete(key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Multiple views may subscribe independently; notifications occur only after settle. */
|
/** Multiple views may subscribe independently; notifications occur only after settle. */
|
||||||
@@ -274,8 +237,7 @@ class DelayManager {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// React's external-store reads must retain identity until the value changes.
|
return { ...entry }
|
||||||
return entry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDelay(name: string, group: string) {
|
getDelay(name: string, group: string) {
|
||||||
@@ -314,21 +276,19 @@ class DelayManager {
|
|||||||
member: InteractableProxyMember,
|
member: InteractableProxyMember,
|
||||||
group: string,
|
group: string,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
): Promise<void> {
|
): Promise<DelayUpdate> {
|
||||||
const generation = this.generation
|
|
||||||
// Entry points own the pending mark; measureDelay only writes results.
|
// Entry points own the pending mark; measureDelay only writes results.
|
||||||
this.setDelay(member.ref.name, group, -2)
|
this.setDelay(member.ref.name, group, -2)
|
||||||
await this.measureDelay(member, group, timeout, generation)
|
const update = await this.measureDelay(member, group, timeout)
|
||||||
if (generation === this.generation) this.queueGroupNotification(group)
|
this.queueGroupNotification(group)
|
||||||
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
private async measureDelay(
|
private async measureDelay(
|
||||||
member: InteractableProxyMember,
|
member: InteractableProxyMember,
|
||||||
group: string,
|
group: string,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
generation: number,
|
): Promise<DelayUpdate> {
|
||||||
): Promise<void> {
|
|
||||||
if (generation !== this.generation) return
|
|
||||||
const name = member.ref.name
|
const name = member.ref.name
|
||||||
const providerName =
|
const providerName =
|
||||||
member.kind === 'node' ? providerNameOf(member.node) : undefined
|
member.kind === 'node' ? providerNameOf(member.node) : undefined
|
||||||
@@ -364,18 +324,14 @@ class DelayManager {
|
|||||||
const elapsed = elapsedTime
|
const elapsed = elapsedTime
|
||||||
debugLog(`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`)
|
debugLog(`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`)
|
||||||
|
|
||||||
if (generation === this.generation) {
|
return this.setDelay(name, group, delay, { elapsed })
|
||||||
this.setDelay(name, group, delay, { elapsed })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error)
|
console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error)
|
||||||
const delay = 1e6 // error
|
const delay = 1e6 // error
|
||||||
const elapsed = Date.now() - startTime
|
const elapsed = Date.now() - startTime
|
||||||
|
|
||||||
if (generation === this.generation) {
|
return this.setDelay(name, group, delay, { elapsed })
|
||||||
this.setDelay(name, group, delay, { elapsed })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +341,6 @@ class DelayManager {
|
|||||||
timeout: number,
|
timeout: number,
|
||||||
concurrency = 36,
|
concurrency = 36,
|
||||||
) {
|
) {
|
||||||
const generation = this.generation
|
|
||||||
debugLog(
|
debugLog(
|
||||||
`[DelayManager] 批量测试延迟开始,组: ${group}, 数量: ${proxies.length}, 并发数: ${concurrency}`,
|
`[DelayManager] 批量测试延迟开始,组: ${group}, 数量: ${proxies.length}, 并发数: ${concurrency}`,
|
||||||
)
|
)
|
||||||
@@ -399,7 +354,6 @@ class DelayManager {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
const help = async (): Promise<void> => {
|
const help = async (): Promise<void> => {
|
||||||
if (generation !== this.generation) return
|
|
||||||
const currMember = proxies[index++]
|
const currMember = proxies[index++]
|
||||||
if (!currMember) return
|
if (!currMember) return
|
||||||
const currName = currMember.ref.name
|
const currName = currMember.ref.name
|
||||||
@@ -413,15 +367,13 @@ class DelayManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do not reorder a list around the pointer while batch results are arriving.
|
// Do not reorder a list around the pointer while batch results are arriving.
|
||||||
await this.measureDelay(currMember, group, timeout, generation)
|
await this.measureDelay(currMember, group, timeout)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`[DelayManager] 批量测试单个代理出错,代理: ${currName}`,
|
`[DelayManager] 批量测试单个代理出错,代理: ${currName}`,
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
if (generation === this.generation) {
|
this.setDelay(currName, group, 1e6)
|
||||||
this.setDelay(currName, group, 1e6)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return help()
|
return help()
|
||||||
@@ -439,14 +391,12 @@ class DelayManager {
|
|||||||
await Promise.all(promiseList)
|
await Promise.all(promiseList)
|
||||||
} finally {
|
} finally {
|
||||||
// Always release the batch and notify; otherwise failures leave stale sort state.
|
// Always release the batch and notify; otherwise failures leave stale sort state.
|
||||||
if (generation === this.generation) {
|
const remaining = (this.activeBatches.get(group) ?? 1) - 1
|
||||||
const remaining = (this.activeBatches.get(group) ?? 1) - 1
|
if (remaining > 0) {
|
||||||
if (remaining > 0) {
|
this.activeBatches.set(group, remaining)
|
||||||
this.activeBatches.set(group, remaining)
|
} else {
|
||||||
} else {
|
this.activeBatches.delete(group)
|
||||||
this.activeBatches.delete(group)
|
this.queueGroupNotification(group)
|
||||||
this.queueGroupNotification(group)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const totalTime = Date.now() - startTime
|
const totalTime = Date.now() - startTime
|
||||||
|
|||||||
Reference in New Issue
Block a user