Compare commits

..
63 changed files with 565 additions and 264 deletions
-1
View File
@@ -1,3 +1,2 @@
avoid-breaking-exported-api = true
allow-unwrap-in-tests = true
cognitive-complexity-threshold = 25
+2 -2
View File
@@ -4,11 +4,11 @@
"features": {
"ghcr.io/devcontainers/features/node:2": {
"version": "26.13.1",
"version": "26.13.0",
"pnpmVersion": "12.18.0"
},
"ghcr.io/devcontainers/features/rust:1": {
"version": "1.100.0",
"version": "1.99.1",
"profile": "default"
},
"ghcr.io/devcontainers/features/git:1": {},
+17 -11
View File
@@ -51,7 +51,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -154,7 +154,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -182,7 +182,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -191,7 +191,9 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
run: |
pnpm release-version autobuild-latest
pnpm polyfills:generate
- name: Tauri build for Windows-macOS-Linux
uses: tauri-apps/tauri-action@v1
@@ -240,7 +242,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -262,7 +264,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -271,7 +273,9 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
run: |
pnpm release-version autobuild-latest
pnpm polyfills:generate
- name: 'Setup for linux'
run: |-
@@ -373,7 +377,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -395,7 +399,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -404,7 +408,9 @@ jobs:
pnpm run prebuild ${{ matrix.target }}
- name: Release ${{ env.TAG_CHANNEL }} Version
run: pnpm release-version autobuild-latest
run: |
pnpm release-version autobuild-latest
pnpm polyfills:generate
- name: Download WebView2 Runtime
run: |
@@ -479,7 +485,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6.1.1
name: Install pnpm
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
+2 -2
View File
@@ -33,7 +33,7 @@ jobs:
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
with:
version: v1.2.2
version: v1.0.1
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
with:
@@ -49,7 +49,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Install frontend dependencies
env:
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+6 -4
View File
@@ -74,7 +74,7 @@ jobs:
- name: Install Rust Stable
if: github.event.inputs[matrix.input] == 'true'
uses: dtolnay/rust-toolchain@1.100.0
uses: dtolnay/rust-toolchain@1.99.1
- name: Rust Cache
if: github.event.inputs[matrix.input] == 'true'
@@ -104,7 +104,7 @@ jobs:
if: github.event.inputs[matrix.input] == 'true'
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -115,13 +115,15 @@ jobs:
- name: Release ${{ env.TAG_CHANNEL }} Version
if: github.event.inputs[matrix.input] == 'true'
run: pnpm release-version ${{ env.TAG_NAME }}
run: |
pnpm release-version ${{ env.TAG_NAME }}
pnpm polyfills:generate
- name: Add Rust Target
if: github.event.inputs[matrix.input] == 'true'
run: |
# Ensure cross target is installed for the pinned toolchain; fallback without explicit toolchain if needed
rustup target add ${{ matrix.target }} --toolchain 1.100.0 || rustup target add ${{ matrix.target }}
rustup target add ${{ matrix.target }} --toolchain 1.99.1 || rustup target add ${{ matrix.target }}
rustup target list --installed
echo "Rust target ${{ matrix.target }} installed."
+4 -1
View File
@@ -36,7 +36,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
@@ -58,6 +58,9 @@ jobs:
- name: Run TypeScript typecheck
run: pnpm typecheck
- name: Verify modern polyfill inventory against automatic discovery
run: pnpm polyfills:check
- name: Run tests
run: pnpm test
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
- uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
if: github.event.action != 'closed'
+11 -11
View File
@@ -46,7 +46,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- name: Validate source and publish the approved snapshot
id: source
env:
@@ -203,7 +203,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -231,7 +231,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -308,7 +308,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -330,7 +330,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -439,7 +439,7 @@ jobs:
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: ${{ matrix.target }}
- name: Rust Cache
@@ -461,7 +461,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
cache: 'pnpm'
- name: Pnpm install and check
@@ -542,7 +542,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
with:
run_install: false
@@ -565,7 +565,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -593,7 +593,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -661,7 +661,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -22,13 +22,13 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.100.0'
toolchain: '1.99.1'
targets: x86_64-pc-windows-msvc
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- name: Install pnpm
uses: pnpm/action-setup@v6
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
@@ -39,7 +39,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v7
with:
node-version: '26.13.1'
node-version: '26.13.0'
- uses: pnpm/action-setup@v6
name: Install pnpm
+2 -2
View File
@@ -1,3 +1,3 @@
nodejs 26.13.1
nodejs 26.13.0
pnpm 12.18.0
rust 1.100.0
rust 1.99.1
+11
View File
@@ -101,6 +101,17 @@ For frontend-only development, use `pnpm web:dev`. It does not start the Rust ba
### Build the Project
`pnpm web:build` uses a generated modern-polyfill inventory and rejects changed
emitted module inputs, output, targets, or locked dependencies before writing
output. Inputs include dependency code and code later removed by minification.
When it reports a stale inventory, run `pnpm polyfills:generate` and commit
`scripts/modern-polyfills.json`. Generation preserves automatic discovery's import
order and requires byte-identical automatic/explicit outputs for the macOS, Linux,
and Windows production defines. CI repeats this comparison with
`pnpm polyfills:check`; browser targets and worker configuration stay unchanged.
Release build jobs regenerate after rewriting versions because version data is
also emitted in frontend modules.
Standard build:
```bash
Generated
+7 -6
View File
@@ -1123,6 +1123,7 @@ dependencies = [
"rfd 0.18.0",
"runas",
"rustls",
"scopeguard",
"serde",
"serde_json",
"serde_yaml_ng",
@@ -7072,9 +7073,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.40.3"
version = "0.40.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a86bac2f7d06e6384984118ad727f5694110f0ec44d924d05c8fdec3d030bee6"
checksum = "0e8a3edbe0394ce115f3917a00349dd242be09eb5a091ed509eaf67b55cfb8a8"
dependencies = [
"libc",
"memchr",
@@ -9087,9 +9088,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.12"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7265c9301c16a8711c992c2d8da90ee77ef54b1ed51482d6dfeb00dc756b1297"
checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171"
dependencies = [
"rustls-pki-types",
]
@@ -10125,9 +10126,9 @@ dependencies = [
[[package]]
name = "zip"
version = "9.5.1"
version = "9.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3476fd497ac8af5b1ec7b51acab7bef380f7e8dd8e0487595bafd839417a302"
checksum = "0700e6c427aff64acdbf02e85fbfd946c2ee12319d1af013092f3a2178914ff8"
dependencies = [
"aes",
"bzip2",
-1
View File
@@ -132,7 +132,6 @@ large_const_arrays = "warn"
large_futures = "deny"
large_stack_arrays = "warn"
large_types_passed_by_value = "deny"
map_unwrap_or = "deny"
missing_const_for_fn = "deny"
mutex_atomic = "deny"
mutex_integer = "deny"
-3
View File
@@ -29,20 +29,17 @@
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
- 修复窗口较窄时,首页「当前节点」卡片中过长的节点名把延迟标签挤出卡片
**🍎 macOS**
- 修复 macOS TUN 模式下原有 DNS 被清除、关闭后 DNS 未恢复及联网后设置未生效
- 修复 macOS 断网时误报代理已回退直连,恢复联网后自动重试系统代理
- 修复 macOS 切换主题后标题栏保留原有深浅色
</details>
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增连接页面按网络协议(TCP/UDP)筛选连接
</details>
+4 -4
View File
@@ -206,10 +206,10 @@ impl Logger {
.map(|s| (*s).to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Unknown panic payload".to_string());
let location = info.location().map_or_else(
|| "Unknown location".to_string(),
|loc| format!("{}:{}", loc.file(), loc.line()),
);
let location = info
.location()
.map(|loc| format!("{}:{}", loc.file(), loc.line()))
.unwrap_or_else(|| "Unknown location".to_string());
logging!(error, Type::System, "Panic occurred at {}: {}", location, payload);
Self::global().flush_logs();
std::thread::sleep(Duration::from_millis(100));
@@ -13,13 +13,14 @@ pub(super) async fn check_chatgpt(client: &Client) -> UnlockItem {
let web_status = get_text(client, "https://api.openai.com/compliance/cookie_requirements")
.await
.map_or("Failed", |body| {
.map(|body| {
if body.to_ascii_lowercase().contains("unsupported_country") {
"Unsupported Country/Region"
} else {
"Yes"
}
});
})
.unwrap_or("Failed");
UnlockItem::checked(CHATGPT_WEB_NAME, web_status, region)
}
@@ -157,10 +157,11 @@ async fn is_unavailable(client: &Client) -> bool {
.get("https://disneyplus.com")
.send()
.await
.map_or(true, |response| {
.map(|response| {
let url = response.url().as_str();
url.contains("preview") || url.contains("unavailable")
})
.unwrap_or(true)
}
async fn fallback_region(client: &Client) -> Option<UnlockItem> {
@@ -114,10 +114,11 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
}
match response.json::<FastResponse>().await {
Ok(data) => data.targets.first().map_or_else(
|| item("Unknown"),
|target| UnlockItem::checked_region(NETFLIX_NAME, "Yes", &target.location.country),
),
Ok(data) => data
.targets
.first()
.map(|target| UnlockItem::checked_region(NETFLIX_NAME, "Yes", &target.location.country))
.unwrap_or_else(|| item("Unknown")),
Err(e) => {
logging!(error, Type::Network, "解析 Fast.com API 响应失败: {e}");
item("Failed (解析错误)")
@@ -12,7 +12,7 @@ parking_lot = { workspace = true }
serde = { workspace = true, features = ["derive"] }
# sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.40.3", default-features = false, features = ["network", "system"] }
sysinfo = { version = "0.40.1", default-features = false, features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.191"
@@ -118,7 +118,8 @@ fn is_binary_admin() -> bool {
#[cfg(windows)]
Token::with_current_process()
.and_then(|token| token.privilege_level())
.is_ok_and(|level| level != PrivilegeLevel::NotPrivileged)
.map(|level| level != PrivilegeLevel::NotPrivileged)
.unwrap_or(false)
}
#[inline]
+3 -1
View File
@@ -15,6 +15,8 @@
"tauri": "tauri",
"web:dev": "vite",
"web:build": "tsrs -p . --noEmit && vite build",
"polyfills:generate": "node scripts/generate-modern-polyfills.mjs --write",
"polyfills:check": "node scripts/generate-modern-polyfills.mjs --check",
"web:serve": "vite preview",
"prebuild": "node scripts/prebuild.mjs",
"updater": "node scripts/updater.mjs",
@@ -121,7 +123,7 @@
"https-proxy-agent": "^9.1.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.45.1",
"knip": "6.44.2",
"lint-staged": "^17.5.1",
"sass": "^1.104.1",
"tar": "^7.5.22",
+10 -9
View File
@@ -396,8 +396,8 @@ importers:
specifier: ^2.7.0
version: 2.7.0
knip:
specifier: 6.45.1
version: 6.45.1
specifier: 6.44.2
version: 6.44.2
lint-staged:
specifier: ^17.5.1
version: 17.5.1
@@ -3856,8 +3856,8 @@ packages:
keyv@5.6.0:
resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
knip@6.45.1:
resolution: {integrity: sha512-zp3zXKy2cZWiIttdlWU5fisKxVQaLSU6471ej0KGz1GdHdaHxpzTAO/LIyFNc3gU3v1W7LJaqvYumoO3l/u+6A==}
knip@6.44.2:
resolution: {integrity: sha512-6URO7pnqtOnyvSsXfW6+8C9j1lfcicxUCysXO0RgMtHTZ96HqCXh29op8iabeQW5JeIc8dJWwzSdJVWDokeScQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -4804,8 +4804,8 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
unbash@6.0.4:
resolution: {integrity: sha512-t3OptUArewg9OpYGDskTSlxhyLDahfmZ4d36hE44Efb1y3HRLLpzsVqwH+AwBVymP4t7pP71xN+nnduudn2awA==}
unbash@6.0.1:
resolution: {integrity: sha512-0db9eS5iqybux41zLkX69nk90KTci7F7eZ+gXzEG/Q7E0tAqEBhRh5fkZvk1N7jT7QkGWqq34i8SFGPFEe1V8A==}
engines: {node: '>=14'}
undici-types@6.21.0:
@@ -8684,7 +8684,7 @@ snapshots:
dependencies:
'@keyv/serialize': 1.1.1
knip@6.45.1:
knip@6.44.2:
dependencies:
formatly: 0.11.4
get-tsconfig: 4.14.3
@@ -8695,8 +8695,9 @@ snapshots:
smol-toml: 1.9.1
strip-json-comments: 5.0.3
tinyglobby: 0.2.17
unbash: 6.0.4
unbash: 6.0.1
yaml: 2.9.1
zod: 4.6.5
lazystream@1.0.1:
dependencies:
@@ -9786,7 +9787,7 @@ snapshots:
typescript@6.0.3: {}
unbash@6.0.4: {}
unbash@6.0.1: {}
undici-types@6.21.0: {}
+1 -1
View File
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.100.0"
channel = "1.99.1"
components = ["rustfmt", "clippy"]
+80
View File
@@ -0,0 +1,80 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { build } from 'vite'
const [command, ...extra] = process.argv.slice(2)
if (!['--write', '--check'].includes(command) || extra.length) {
throw new Error(
'Usage: node scripts/generate-modern-polyfills.mjs --write|--check',
)
}
const inventoryPath = 'scripts/modern-polyfills.json'
const staleMessage =
'Modern polyfill inventory is stale; run pnpm polyfills:generate and commit scripts/modern-polyfills.json'
function outputHashes(result) {
assert(
!Array.isArray(result) && 'output' in result,
'Expected one app output',
)
return result.output
.map((file) => [
file.fileName,
createHash('sha256')
.update(file.type === 'chunk' ? file.code : file.source)
.digest('hex'),
])
.sort(([a], [b]) => a.localeCompare(b))
}
const temporary = mkdtempSync(path.join(os.tmpdir(), 'modern-polyfills-'))
const capture = path.join(temporary, 'capture.json')
const candidate = path.join(temporary, 'inventory.json')
const snapshots = []
const fingerprints = []
let imports
try {
process.env.MODERN_POLYFILLS_CAPTURE = capture
// OS_PLATFORM changes emitted code; certify each production define without
// launching a native app or normalizing away code the detector actually sees.
for (const platform of ['darwin', 'linux', 'win32']) {
const config = {
logLevel: 'error',
define: { OS_PLATFORM: JSON.stringify(platform) },
build: { write: false },
}
const automatic = await build(config)
const detected = JSON.parse(readFileSync(capture, 'utf8'))
if (imports) assert.deepEqual(detected.imports, imports)
imports = detected.imports
fingerprints.push(detected.fingerprint)
snapshots.push({ config, hashes: outputHashes(automatic) })
}
const content = `${JSON.stringify({ imports, fingerprints: [...new Set(fingerprints)].sort() }, null, 2)}\n`
writeFileSync(candidate, content)
delete process.env.MODERN_POLYFILLS_CAPTURE
process.env.MODERN_POLYFILLS_INVENTORY = candidate
for (const { config, hashes } of snapshots) {
assert.deepEqual(
outputHashes(await build(config)),
hashes,
'Explicit polyfills must be byte-identical to automatic discovery',
)
}
if (command === '--check') {
assert.equal(readFileSync(inventoryPath, 'utf8'), content, staleMessage)
} else {
writeFileSync(inventoryPath, content)
}
console.log(
`Modern polyfills ${command === '--check' ? 'verified' : 'generated'}: ${imports.length} imports; all three OS outputs byte-identical to automatic discovery`,
)
} finally {
delete process.env.MODERN_POLYFILLS_CAPTURE
delete process.env.MODERN_POLYFILLS_INVENTORY
rmSync(temporary, { recursive: true })
}
+72
View File
@@ -0,0 +1,72 @@
{
"imports": [
"core-js/modules/es.error.cause.js",
"core-js/modules/es.array.at.js",
"core-js/modules/es.array.includes.js",
"core-js/modules/es.array.push.js",
"core-js/modules/es.array.unshift.js",
"core-js/modules/es.array-buffer.detached.js",
"core-js/modules/es.array-buffer.transfer.js",
"core-js/modules/es.array-buffer.transfer-to-fixed-length.js",
"core-js/modules/es.iterator.constructor.js",
"core-js/modules/es.iterator.every.js",
"core-js/modules/es.iterator.filter.js",
"core-js/modules/es.iterator.find.js",
"core-js/modules/es.iterator.flat-map.js",
"core-js/modules/es.iterator.for-each.js",
"core-js/modules/es.iterator.map.js",
"core-js/modules/es.iterator.reduce.js",
"core-js/modules/es.iterator.some.js",
"core-js/modules/es.iterator.to-array.js",
"core-js/modules/es.json.parse.js",
"core-js/modules/es.json.stringify.js",
"core-js/modules/es.map.get-or-insert.js",
"core-js/modules/es.map.get-or-insert-computed.js",
"core-js/modules/es.regexp.flags.js",
"core-js/modules/es.set.difference.v2.js",
"core-js/modules/es.set.intersection.v2.js",
"core-js/modules/es.set.is-disjoint-from.v2.js",
"core-js/modules/es.set.is-subset-of.v2.js",
"core-js/modules/es.set.is-superset-of.v2.js",
"core-js/modules/es.set.symmetric-difference.v2.js",
"core-js/modules/es.set.union.v2.js",
"core-js/modules/es.string.at-alternative.js",
"core-js/modules/es.typed-array.at.js",
"core-js/modules/es.typed-array.fill.js",
"core-js/modules/es.typed-array.find-last.js",
"core-js/modules/es.typed-array.find-last-index.js",
"core-js/modules/es.typed-array.set.js",
"core-js/modules/es.typed-array.sort.js",
"core-js/modules/es.typed-array.to-reversed.js",
"core-js/modules/es.typed-array.to-sorted.js",
"core-js/modules/es.typed-array.with.js",
"core-js/modules/es.uint8-array.set-from-base64.js",
"core-js/modules/es.uint8-array.set-from-hex.js",
"core-js/modules/es.uint8-array.to-base64.js",
"core-js/modules/es.uint8-array.to-hex.js",
"core-js/modules/es.weak-map.get-or-insert.js",
"core-js/modules/es.weak-map.get-or-insert-computed.js",
"core-js/modules/esnext.array.group.js",
"core-js/modules/web.dom-exception.stack.js",
"core-js/modules/web.immediate.js",
"core-js/modules/web.url-search-params.delete.js",
"core-js/modules/web.url-search-params.has.js",
"core-js/modules/web.url-search-params.size.js",
"core-js/modules/es.object.has-own.js",
"core-js/modules/es.array.with.js",
"core-js/modules/es.symbol.async-dispose.js",
"core-js/modules/es.async-iterator.async-dispose.js",
"core-js/modules/es.symbol.dispose.js",
"core-js/modules/es.iterator.dispose.js",
"core-js/modules/es.aggregate-error.cause.js",
"core-js/modules/es.array.find-last.js",
"core-js/modules/es.iterator.drop.js",
"core-js/modules/es.iterator.take.js",
"core-js/modules/web.structured-clone.js"
],
"fingerprints": [
"30dd6a7f16e7065fa9803260e11aa2f2db56cabb69664257c940ab05bae8505b",
"5630fe8a486687073b6d9894dd39735b77d5d8177432a4ada81947a429ff6991",
"ddbb980f9ad3fe92ee7c30248d46ed4cdeb421c714266e627b825b59042c30bc"
]
}
+139
View File
@@ -0,0 +1,139 @@
import { createHash } from 'node:crypto'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import legacy, { detectPolyfills } from '@vitejs/plugin-legacy'
const digest = (value) => createHash('sha256').update(value).digest('hex')
const inventoryPath = 'scripts/modern-polyfills.json'
const staleMessage =
'Modern polyfill inventory is stale; run pnpm polyfills:generate and commit scripts/modern-polyfills.json'
// Bind complete emitted module inputs as well as output: minification can hide APIs.
// Generation double-checks the exported detector against legacy's automatic build.
export function guardedLegacy(options) {
const capturePath = process.env.MODERN_POLYFILLS_CAPTURE
const inventory = capturePath
? null
: JSON.parse(
readFileSync(
process.env.MODERN_POLYFILLS_INVENTORY || inventoryPath,
'utf8',
),
)
const plugins = legacy({
...options,
modernPolyfills: Boolean(capturePath),
additionalModernPolyfills: [
...options.additionalModernPolyfills,
...(capturePath ? [] : inventory.imports),
],
})
const postProcess = plugins.find(
(plugin) => plugin.name === 'vite:legacy-post-process',
)
if (!postProcess?.renderChunk) {
throw new Error('plugin-legacy must provide its post-process render hook')
}
const original = postProcess.renderChunk
const render = typeof original === 'function' ? original : original.handler
const discovered = new Map()
let chunkNames
let context
if (capturePath) {
postProcess.renderChunk = async function (raw, chunk, output, meta) {
chunkNames ??= Object.keys(meta.chunks)
const imports = new Set()
await detectPolyfills(
raw,
options.modernTargets,
options.assumptions || {},
imports,
)
discovered.set(chunk.fileName, imports)
return render.call(this, raw, chunk, output, meta)
}
}
return [
...plugins,
{
name: 'guard-modern-polyfills',
enforce: 'post',
apply: 'build',
configResolved(config) {
if (config.build.ssr || options.renderLegacyChunks !== false) {
throw new Error(
'Modern polyfill guard requires a modern-only app build',
)
}
// Custom import paths must not bind the inventory to a checkout location.
context = [
{
...options,
additionalModernPolyfills: options.additionalModernPolyfills.map(
(file) =>
path.relative(process.cwd(), file).split(path.sep).join('/'),
),
},
config.build.target,
config.mode,
...[
'package.json',
'pnpm-lock.yaml',
'vite.config.mts',
'scripts/modern-polyfills.mjs',
'scripts/generate-modern-polyfills.mjs',
].map((file) => digest(readFileSync(file))),
...[
'.browserslistrc',
'browserslist',
'src/.browserslistrc',
'src/browserslist',
].map((file) =>
existsSync(file) ? digest(readFileSync(file)) : null,
),
digest(
readFileSync(new URL(import.meta.resolve('@vitejs/plugin-legacy'))),
),
]
},
generateBundle(_output, bundle) {
const imports = capturePath
? [
...new Set(
chunkNames.flatMap((name) => [...discovered.get(name)]),
),
]
: inventory.imports
const chunks = Object.values(bundle).filter(
(file) => file.type === 'chunk',
)
const moduleIds = new Set(
chunks.flatMap((chunk) => Object.keys(chunk.modules)),
)
const inputs = [...moduleIds].map((id) => {
// Rolldown's runtime is generated, not a transformed input module.
if (id === '\0rolldown/runtime.js') return null
// Vite's HTML entry code embeds an absolute import to the main module.
if (id.endsWith('.html')) return digest(readFileSync(id))
const code = this.getModuleInfo(id)?.code
if (code == null) this.error(`Missing emitted module input: ${id}`)
return digest(code)
})
const fingerprint = digest(
JSON.stringify([
context,
inputs.sort(),
chunks.map((chunk) => digest(chunk.code)).sort(),
imports,
]),
)
if (capturePath) {
writeFileSync(capturePath, JSON.stringify({ fingerprint, imports }))
} else if (!inventory.fingerprints.includes(fingerprint)) {
this.error(staleMessage)
}
},
},
]
}
+4 -3
View File
@@ -8,7 +8,7 @@ repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
build = "build.rs"
edition = "2024"
rust-version = "1.100"
rust-version = "1.99"
[lib]
name = "app_lib"
@@ -77,7 +77,7 @@ tauri-plugin-dialog = "2.8.4"
tauri-plugin-fs = "2.7.1"
tauri-plugin-deep-link = "2.6.3"
tauri-plugin-window-state = "2.5.0"
zip = "9.5.1"
zip = "9.5.0"
flate2 = "1.1.11"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.1" }
@@ -86,6 +86,7 @@ sha2 = "0.11.1"
getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.8.0"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.8.3"
@@ -99,7 +100,7 @@ clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verg
arc-swap = "1.9.5"
tokio-rustls = "0.26.6"
rustls = { version = "0.23.46", features = ["ring"] }
webpki-roots = "1.0.12"
webpki-roots = "1.0.11"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.20", features = ["time"] }
-2
View File
@@ -17,8 +17,6 @@
"autostart:allow-disable",
"autostart:allow-is-enabled",
"core:window:allow-set-theme",
"core:window:allow-set-background-color",
"core:webview:allow-set-webview-background-color",
"notification:default",
"http:default",
"http:allow-fetch",
+4 -4
View File
@@ -2,10 +2,10 @@
// bundles (PR #8131), so this generator must stay an example, never a bin.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args_os().skip(1);
let output = args.next().map_or_else(
|| std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"),
std::path::PathBuf::from,
);
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());
}
+4 -6
View File
@@ -18,11 +18,9 @@ use crate::{
};
use clash_verge_draft::{Draft, SharedDraft};
use clash_verge_logging::{Type, logging, logging_error};
use scopeguard::defer;
use smartstring::alias::String;
use std::{
mem::DropGuard,
sync::atomic::{AtomicBool, Ordering},
};
use std::sync::atomic::{AtomicBool, Ordering};
static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false);
@@ -402,9 +400,9 @@ pub async fn patch_profiles_config(profiles: ProfileSelection) -> CmdResult<Vali
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(ValidationOutcome::Busy);
}
let _switching_profile = DropGuard::new((), |()| {
defer! {
CURRENT_SWITCHING_PROFILE.store(false, Ordering::Release);
});
}
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = IProfiles {
+2 -1
View File
@@ -83,7 +83,8 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
.get("exclude-type")
.and_then(|types| types.as_str())
.filter(|types| !types.is_empty())
.map_or_default(|types| types.split('|').map(str::to_owned).collect());
.map(|types| types.split('|').map(str::to_owned).collect())
.unwrap_or_default();
Some((
name,
GroupScope {
+1 -1
View File
@@ -191,7 +191,7 @@ impl Config {
}
}
} else {
let error_msg = config_result.err().map_or_default(|err| err.to_string());
let error_msg = config_result.err().map(|err| err.to_string()).unwrap_or_default();
logging!(warn, Type::Config, "生成配置文件失败,使用默认配置: {error_msg}");
CoreManager::global()
.use_default_config(NoticeStatus::ConfigValidateError, "")
+3 -4
View File
@@ -371,10 +371,9 @@ impl PrfItem {
let uid = help::get_uid("R").into();
let file = format!("{uid}.yaml").into();
let name = name.map_or_else(
|| filename.map_or_else(|| "Remote File".into(), |s| s.into()),
|s| s.to_owned(),
);
let name = name
.map(|s| s.to_owned())
.unwrap_or_else(|| filename.map(|s| s.into()).unwrap_or_else(|| "Remote File".into()));
let data = resp.text();
let data = data.trim_start_matches('\u{feff}');
+2 -1
View File
@@ -279,7 +279,8 @@ async fn cleanup_auto_backups() -> Result<()> {
.and_then(|meta| meta.modified())
.ok()
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map_or(0, |dur| dur.as_secs());
.map(|dur| dur.as_secs())
.unwrap_or(0);
files.push((path, modified));
}
+6 -4
View File
@@ -539,16 +539,18 @@ fn bind_socket(claim: &BindClaim) -> io::Result<Socket> {
}
fn is_bind_conflict(error: &io::Error) -> bool {
if error.kind() == io::ErrorKind::AddrInUse {
return true;
}
#[cfg(windows)]
{
use windows_sys::Win32::Networking::WinSock::WSAEACCES;
error.kind() == io::ErrorKind::AddrInUse || error.raw_os_error() == Some(WSAEACCES)
error.raw_os_error() == Some(WSAEACCES)
}
#[cfg(not(windows))]
{
error.kind() == io::ErrorKind::AddrInUse
}
false
}
#[cfg(windows)]
+5 -5
View File
@@ -14,10 +14,10 @@ use anyhow::{Result, anyhow};
use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use clash_verge_service_ipc::StageRuntimeOutcome;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
collections::HashSet,
mem::DropGuard,
path::PathBuf,
sync::Arc,
time::{Duration, Instant},
@@ -191,9 +191,9 @@ impl CoreManager {
logging!(debug, Type::Core, "Configuration update is already running");
return Ok(ValidationOutcome::Busy);
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
if !force && !self.should_update_config() {
logging!(debug, Type::Core, "Skipping config update due to debounce");
@@ -314,9 +314,9 @@ impl CoreManager {
logging!(debug, Type::Core, "Configuration update is already running");
return Ok(ValidationOutcome::Busy);
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
let runtime = Config::runtime().await;
let transaction = DraftTransaction::begin(vec![&runtime])?;
+12 -11
View File
@@ -6,8 +6,9 @@ use crate::core::proxy_control::{self, SysproxyFailure};
use crate::core::service::{SERVICE_MANAGER, ServiceStatus};
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use smartstring::alias::String;
use std::{mem::DropGuard, path::Path};
use std::path::Path;
#[cfg(target_os = "windows")]
use tauri_plugin_clash_verge_sysinfo::is_current_app_handle_admin;
@@ -355,9 +356,9 @@ impl CoreManager {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
self.start_core_during_config_update().await
}
@@ -382,9 +383,9 @@ impl CoreManager {
service_rejection,
));
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
// Lock order is config then lifecycle. disable_tun_and_persist commits the shared Verge
// draft without claiming it, so without this it can commit another transaction's staged patch.
let config_write = Config::lock_config_write().await;
@@ -462,9 +463,9 @@ impl CoreManager {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
// Lock order is config then lifecycle, for the unclaimed draft write below.
let _config_write = Config::lock_config_write().await;
let _life = self.lifecycle_lock.lock().await;
@@ -794,9 +795,9 @@ impl CoreManager {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
self.restart_core_during_config_update().await
}
@@ -965,9 +966,9 @@ impl CoreManager {
if !self.try_start_config_update() {
return HandoffOutcome::NotReady;
}
let _config_update = DropGuard::new((), |()| {
defer! {
self.finish_config_update();
});
}
// Lock order is config then lifecycle.
let _life = self.lifecycle_lock.lock().await;
+3 -1
View File
@@ -331,7 +331,9 @@ fn build_provider_records(
providers: Option<ProxyProviders>,
records: &mut BTreeMap<String, ProxyNodeView>,
) -> (Vec<ProxyProviderView>, ProviderCandidates) {
let providers = providers.map_or_default(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>());
let providers = providers
.map(|providers| providers.providers.into_iter().collect::<BTreeMap<_, _>>())
.unwrap_or_default();
let mut views = Vec::new();
let mut candidates = BTreeMap::new();
+2 -1
View File
@@ -126,7 +126,8 @@ fn allocate_provider_destination(destination: &str, url: &str, reserved: &mut Ha
let path = Path::new(destination);
let extension = path
.extension()
.map_or_default(|ext| format!(".{}", ext.to_string_lossy()));
.map(|ext| format!(".{}", ext.to_string_lossy()))
.unwrap_or_default();
let mut suffix = String::new();
let mut sequence = 0;
loop {
+4 -1
View File
@@ -1470,7 +1470,10 @@ async fn create_sync_temp(target: &Path) -> Result<(PathBuf, tokio::fs::File)> {
.await
.with_context(|| format!("failed to create provider cache directory {}", parent.display()))?;
}
let name = target.file_name().map_or_default(|name| name.to_string_lossy());
let name = target
.file_name()
.map(|name| name.to_string_lossy())
.unwrap_or_default();
use crate::utils::retry::{RetryError, RetryPolicy, retry};
retry(
RetryPolicy::fixed(std::num::NonZeroUsize::MIN.saturating_add(7), std::time::Duration::ZERO),
+3 -3
View File
@@ -7,9 +7,9 @@ use crate::{
use anyhow::Result;
use clash_verge_logging::{Type, logging};
use parking_lot::RwLock;
use scopeguard::defer;
use smartstring::alias::String;
use std::{
mem::DropGuard,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@@ -541,9 +541,9 @@ impl Sysopt {
{
return Ok(());
}
let _reset_sysproxy = DropGuard::new((), |()| {
defer! {
self.reset_sysproxy.store(false, Ordering::SeqCst);
});
}
let mut proxy_state = self.proxy_state_lock.lock().await;
let _guard_operation = self.guard_operation_lock.lock().await;
let drained = self.stop_proxy_guard_locked().await;
+29 -26
View File
@@ -210,7 +210,8 @@ impl Tray {
.latest_arc()
.0
.get("mode")
.map_or("rule", |val| val.as_str().unwrap_or("rule"))
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let profiles_config = Config::profiles().await;
@@ -413,23 +414,26 @@ impl Tray {
}
fn create_hotkeys(hotkeys: &Option<Vec<String>>) -> HashMap<&str, &str> {
hotkeys.as_ref().map_or_default(|h| {
h.iter()
.filter_map(|item| {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => {
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
let uppercase_key = key.to_uppercase();
let unsupported =
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
if unsupported { None } else { Some((func, key)) }
hotkeys
.as_ref()
.map(|h| {
h.iter()
.filter_map(|item| {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => {
// 托盘菜单的 `accelerator` 在 Linux/Windows 下不支持小键盘按键,Windows 下也不支持 Win 键(前端上报为 `CMD`)
let uppercase_key = key.to_uppercase();
let unsupported =
uppercase_key.contains("NUMPAD") || (cfg!(windows) && uppercase_key.contains("CMD"));
if unsupported { None } else { Some((func, key)) }
}
_ => None,
}
_ => None,
}
})
.collect::<HashMap<&str, &str>>()
})
})
.collect::<HashMap<&str, &str>>()
})
.unwrap_or_default()
}
fn create_profile_menu_item(
@@ -489,14 +493,12 @@ fn create_subcreate_proxy_menu_item(
.proxies
.get(proxy_str)
.and_then(|h| h.history.last())
.map_or_else(
|| "-ms".into(),
|h| match h.delay {
0 => "-ms".into(),
delay if delay >= 10000 => "-ms".into(),
_ => format!("{}ms", h.delay),
},
);
.map(|h| match h.delay {
0 => "-ms".into(),
delay if delay >= 10000 => "-ms".into(),
_ => format!("{}ms", h.delay),
})
.unwrap_or_else(|| "-ms".into());
let display_text = format!("{} | {}", proxy_str, delay_text);
@@ -609,7 +611,7 @@ async fn create_tray_menu(
config
.get("proxy-groups")
.and_then(|groups| groups.as_sequence())
.map_or_default(|groups| {
.map(|groups| {
groups
.iter()
.filter_map(|group| group.get("name"))
@@ -618,6 +620,7 @@ async fn create_tray_menu(
.map(|(index, name)| (name.into(), index))
.collect::<HashMap<String, usize>>()
})
.unwrap_or_default()
});
(proxy_nodes_data, runtime_proxy_groups_order)
+5 -4
View File
@@ -1,10 +1,10 @@
use anyhow::Result;
use scopeguard::defer;
use serde::Serialize;
use smartstring::alias::String;
use std::{
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
mem::DropGuard,
sync::LazyLock,
sync::atomic::{AtomicBool, Ordering},
};
@@ -421,9 +421,9 @@ impl CoreConfigValidator {
logging!(info, Type::Validate, "验证已在进行中,跳过新的验证请求");
return Ok(ValidationOutcome::Busy);
}
let _validation = DropGuard::new((), |()| {
defer! {
self.finish();
});
}
logging!(info, Type::Validate, "生成临时配置文件用于验证");
// The test run reads the app dir, which never sees updates made by a service core.
@@ -449,7 +449,8 @@ fn has_ext<P: AsRef<std::path::Path>>(path: P, ext: &str) -> bool {
path.as_ref()
.extension()
.and_then(|s| s.to_str())
.is_some_and(|s| s.eq_ignore_ascii_case(ext))
.map(|s| s.eq_ignore_ascii_case(ext))
.unwrap_or(false)
}
fn contains_any_keyword<'a>(buf: &'a [u8], keywords: &'a [&str]) -> bool {
+15 -9
View File
@@ -634,7 +634,7 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
let proxy_names = config
.get("proxies")
.and_then(|v| v.as_sequence())
.map_or_default(|seq| {
.map(|seq| {
seq.iter()
.filter_map(|item| match item {
Value::Mapping(map) => map
@@ -645,12 +645,13 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
_ => None,
})
.collect::<HashSet<String>>()
});
})
.unwrap_or_default();
let group_names = config
.get("proxy-groups")
.and_then(|v| v.as_sequence())
.map_or_default(|seq| {
.map(|seq| {
seq.iter()
.filter_map(|item| {
item.as_mapping()
@@ -659,17 +660,19 @@ fn cleanup_proxy_groups(mut config: Mapping) -> Mapping {
.map(std::convert::Into::into)
})
.collect::<HashSet<String>>()
});
})
.unwrap_or_default();
let provider_names = config
.get("proxy-providers")
.and_then(Value::as_mapping)
.map_or_default(|map| {
.map(|map| {
map.keys()
.filter_map(Value::as_str)
.map(std::convert::Into::into)
.collect::<HashSet<String>>()
});
})
.unwrap_or_default();
let mut allowed_names = proxy_names;
allowed_names.extend(group_names);
@@ -713,13 +716,15 @@ fn ensure_fake_ip_range6(dns: &mut Mapping) {
let is_fake_ip = dns
.get("enhanced-mode")
.and_then(|v| v.as_str())
.is_none_or(|m| m == "fake-ip");
.map(|m| m == "fake-ip")
.unwrap_or(true);
// Hand-edited YAML may leave the key present but empty.
let range6_missing = dns
.get("fake-ip-range6")
.and_then(|v| v.as_str())
.is_none_or(|s| s.trim().is_empty());
.map(|s| s.trim().is_empty())
.unwrap_or(true);
if ipv6_enabled && is_fake_ip && range6_missing {
dns.insert(Value::from("fake-ip-range6"), Value::from("2001:2::0/64"));
@@ -742,7 +747,8 @@ fn take_mapping(config: &mut Mapping, key: &str) -> Mapping {
config
.get_mut(key)
.and_then(Value::as_mapping_mut)
.map_or_default(std::mem::take)
.map(std::mem::take)
.unwrap_or_default()
}
fn merge_dns_config(mut config: Mapping, mut dns_config: Mapping) -> (Mapping, Mapping) {
+11 -4
View File
@@ -20,10 +20,14 @@ fn collect_proxy_names(seq: &Sequence) -> Vec<String> {
}
fn is_selector_group(group_map: &Mapping) -> bool {
group_map.get("type").and_then(Value::as_str).is_some_and(|value| {
let value = value.to_ascii_lowercase();
value == "select" || value == "selector"
})
group_map
.get("type")
.and_then(Value::as_str)
.map(|value| {
let value = value.to_ascii_lowercase();
value == "select" || value == "selector"
})
.unwrap_or(false)
}
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
@@ -151,6 +155,7 @@ mod tests {
use serde_yaml_ng::Value;
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_delete_proxy_and_references() {
let config_str = r#"
@@ -228,6 +233,7 @@ proxy-groups:
}
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_add_new_proxies_to_first_selector_group() {
let config_str = r#"
@@ -297,6 +303,7 @@ proxy-groups:
}
#[test]
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
fn test_preserve_non_sequence_proxy_groups() {
let config_str = r#"
+4 -2
View File
@@ -235,7 +235,8 @@ pub async fn import_local_backup(source: String) -> Result<String> {
let ext = source_path
.extension()
.and_then(|ext| ext.to_str())
.map_or_default(|ext| ext.to_ascii_lowercase());
.map(|ext| ext.to_ascii_lowercase())
.unwrap_or_default();
if ext != "zip" {
return Err(anyhow!("Only .zip backup files are supported"));
}
@@ -313,7 +314,8 @@ pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
};
let last_modified = metadata
.modified()
.map_or_default(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339());
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
.unwrap_or_default();
backups.push(LocalBackupFile {
filename: file_name.into(),
path: path.to_string_lossy().into(),
+2 -2
View File
@@ -487,8 +487,8 @@ mod tests {
#[test]
fn package_urls_pin_the_resolved_version() {
let release = package_url(false, "v1.19.30").unwrap();
let alpha = package_url(true, "alpha-c0e43eb").unwrap();
let release = package_url(false, "v1.19.30").unwrap_or_default();
let alpha = package_url(true, "alpha-c0e43eb").unwrap_or_default();
assert!(release.contains("/releases/download/v1.19.30/"), "{release}");
assert!(
release.ends_with("-v1.19.30.gz") || release.ends_with("-v1.19.30.zip"),
+1 -1
View File
@@ -44,7 +44,7 @@ fn ensure_icon_cache_target(icon_cache_dir: &Path, file_name: &str) -> CmdResult
}
fn normalized_text_prefix(content: &[u8]) -> std::string::String {
let content = content.trim_prefix(&[0xEF, 0xBB, 0xBF]);
let content = content.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(content);
let start = content
.iter()
.position(|byte| !byte.is_ascii_whitespace())
+7 -7
View File
@@ -19,7 +19,7 @@ use crate::{
use anyhow::{Context as _, Result, anyhow, bail};
use clash_verge_draft::DraftTransaction;
use clash_verge_logging::{Type, logging};
use std::mem::DropGuard;
use scopeguard::{ScopeGuard, defer, guard};
pub async fn probe_listener(request: ListenerProbe) -> Result<ListenerProbeOutcome> {
AsyncHandler::spawn_blocking(move || probe_listener_sync(&request))
@@ -35,9 +35,9 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
if !manager.try_start_config_update() {
bail!("configuration update is already running");
}
let _config_update = DropGuard::new((), |()| {
defer! {
manager.finish_config_update();
});
}
// Read first because transaction rollback discards all claimed drafts.
let current = current_runtime_mapping().await?;
@@ -52,13 +52,13 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
// Hide PAC while the staged ports are not yet serving traffic.
manager.core_starting();
// Recompute PAC state after every exit path.
let _core_start = DropGuard::new((), |()| {
defer! {
manager.core_start_settled();
});
}
// A user-picked port ends the startup fallback, but only once the save lands: until then the
// Core still serves the borrowed port, so every failing exit must put it back or the system
// proxy and PAC point at a dead port. Declared after the PAC guard so it restores first.
let borrowed_port = DropGuard::new(MixedPort::session_fallback(), restore_borrowed_port);
let borrowed_port = guard(MixedPort::session_fallback(), restore_borrowed_port);
MixedPort::clear_session_fallback();
Config::generate()
.await
@@ -125,7 +125,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
transaction.commit();
// The save landed, so the port the app had borrowed is now irrelevant.
let _ = DropGuard::dismiss(borrowed_port);
let _ = ScopeGuard::into_inner(borrowed_port);
announce(Refresh::Clash);
announce(Refresh::Verge);
logging!(info, Type::Config, "Proxy port configuration applied and persisted");
+4 -6
View File
@@ -2,12 +2,10 @@
use crate::core::notify::NoticeStatus;
use std::{
mem::DropGuard,
sync::atomic::{AtomicBool, Ordering},
};
use std::sync::atomic::{AtomicBool, Ordering};
use clash_verge_logging::{Type, logging};
use scopeguard::defer;
use crate::{
config::{Config, IVerge},
@@ -63,9 +61,9 @@ pub async fn reconcile_tun_availability() {
// The flag coalesces newly spawned tasks while the config lock serializes writers.
DISABLING_TUN.store(true, Ordering::Release);
let _disabling_tun = DropGuard::new((), |()| {
defer! {
DISABLING_TUN.store(false, Ordering::Release);
});
}
logging!(
info,
+1 -1
View File
@@ -15,7 +15,7 @@ fn main() -> ExitCode {
#[cfg(not(all(feature = "perf-harness", target_os = "macos")))]
fn main() -> ExitCode {
let default_parallelism = std::thread::available_parallelism().map_or(1, |n| n.get());
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
let worker_limit = std::cmp::min(default_parallelism, 4);
let blocking_limit = 2 * worker_limit;
+3 -3
View File
@@ -3,10 +3,10 @@ use crate::{config::with_encryption, process::AsyncHandler};
use anyhow::{Context as _, Result, anyhow, bail};
use clash_verge_logging::{Type, logging};
use nanoid::nanoid;
use scopeguard::{ScopeGuard, guard};
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value};
use std::{
mem::DropGuard,
path::{Path, PathBuf},
str::FromStr,
};
@@ -168,7 +168,7 @@ pub async fn save_yaml_str(path: &Path, yaml_str: &str) -> Result<()> {
}
};
let temporary = DropGuard::new(temporary, |path| {
let temporary = guard(temporary, |path| {
let _ = std::fs::remove_file(path);
});
let mut file = tokio::fs::File::from_std(file);
@@ -195,7 +195,7 @@ pub async fn save_yaml_str(path: &Path, yaml_str: &str) -> Result<()> {
std::fs::rename(temporary.as_path(), path)
.with_context(|| format!("failed to replace file \"{}\"", path.display()))?;
let _ = DropGuard::dismiss(temporary);
let _ = ScopeGuard::into_inner(temporary);
Ok(())
}
+3 -3
View File
@@ -234,15 +234,15 @@ impl WindowManager {
}
pub fn is_main_window_visible(window: Option<&WebviewWindow<Wry>>) -> bool {
window.is_some_and(|w| w.is_visible().unwrap_or(false))
window.map(|w| w.is_visible().unwrap_or(false)).unwrap_or(false)
}
pub fn is_main_window_focused(window: Option<&WebviewWindow<Wry>>) -> bool {
window.is_some_and(|w| w.is_focused().unwrap_or(false))
window.map(|w| w.is_focused().unwrap_or(false)).unwrap_or(false)
}
fn is_main_window_minimized(window: Option<&WebviewWindow<Wry>>) -> bool {
window.is_some_and(|w| w.is_minimized().unwrap_or(false))
window.map(|w| w.is_minimized().unwrap_or(false)).unwrap_or(false)
}
/// Keep new windows hidden until the frontend overlay renders, avoiding a theme flash.
+2 -8
View File
@@ -1008,13 +1008,8 @@ export const CurrentProxyCard = () => {
border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
}}
>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography
variant="body1"
noWrap
title={currentProxy?.name}
sx={{ fontWeight: 'medium' }}
>
<Box>
<Typography variant="body1" sx={{ fontWeight: 'medium' }}>
{currentProxy?.name ??
t('home.components.currentProxy.labels.noActiveNode')}
</Typography>
@@ -1068,7 +1063,6 @@ export const CurrentProxyCard = () => {
size="small"
label={delayManager.formatDelay(currentDelay)}
color={convertDelayColor(currentDelay)}
sx={{ flexShrink: 0, ml: 1 }}
/>
)}
</Box>
@@ -1106,9 +1106,10 @@ export const EnhancedCanvasTrafficGraph = memo(
{tooltipData.visible && (
<Box
style={{ left: tooltipData.x + 8, top: tooltipData.y - 8 }}
sx={{
position: 'absolute',
left: tooltipData.x + 8,
top: tooltipData.y - 8,
bgcolor: theme.palette.background.paper,
border: 1,
borderColor: 'divider',
@@ -9,7 +9,6 @@ import { useEffect, useMemo } from 'react'
import { useVerge } from '@/hooks/use-verge'
import { defaultDarkTheme, defaultTheme } from '@/pages/_theme'
import { useSetThemeMode, useThemeMode } from '@/services/states'
import getSystem from '@/utils/get-system'
const CSS_INJECTION_SCOPE_ROOT = '[data-css-injection-root]'
const CSS_INJECTION_SCOPE_LIMIT =
@@ -140,12 +139,6 @@ export const useCustomTheme = () => {
console.error(`Failed to set window theme to ${mode}:`, err)
})
}
if (getSystem() === 'macos' && mode) {
const background = mode === 'light' ? defaultTheme : defaultDarkTheme
appWindow.setBackgroundColor(background.background_color).catch((err) => {
console.error('Failed to set window background:', err)
})
}
}, [mode, appWindow, theme_mode])
const theme = useMemo(() => {
+5 -42
View File
@@ -71,10 +71,6 @@ const ORDER_OPTIONS = [
type OrderKey = (typeof ORDER_OPTIONS)[number]['id']
type NetworkFilter = 'all' | 'tcp' | 'udp'
let savedNetworkFilter: NetworkFilter = 'all'
const orderFunctionMap = ORDER_OPTIONS.reduce<Record<OrderKey, OrderFunc>>(
(acc, option) => {
acc[option.id] = option.fn
@@ -90,9 +86,6 @@ const ConnectionsPage = () => {
const { searchState, match, onSearch } = usePageSearch('connections')
const hasSearch = searchState.text.length > 0
const [curOrderOpt, setCurOrderOpt] = useState<OrderKey>('default')
const [networkFilter, setNetworkFilter] =
useState<NetworkFilter>(savedNetworkFilter)
const hasFilter = networkFilter !== 'all'
const [connectionsType, setConnectionsType] = useState<'active' | 'closed'>(
'active',
)
@@ -119,30 +112,18 @@ const ConnectionsPage = () => {
const filterConn = useMemo(() => {
const orderFunc = orderFunctionMap[curOrderOpt]
if (isTableLayout && !hasSearch && !hasFilter) return selectedConnections
if (!hasSearch && !hasFilter) return orderFunc([...selectedConnections])
if (isTableLayout && !hasSearch) return selectedConnections
if (!hasSearch) return orderFunc([...selectedConnections])
const matchConns = selectedConnections.filter((conn) => {
const { host, destinationIP, process, network } = conn.metadata
if (networkFilter !== 'all' && network !== networkFilter) return false
const { host, destinationIP, process } = conn.metadata
return (
!hasSearch ||
match(host || '') ||
match(destinationIP || '') ||
match(process || '')
match(host || '') || match(destinationIP || '') || match(process || '')
)
})
return orderFunc ? orderFunc(matchConns) : matchConns
}, [
selectedConnections,
isTableLayout,
hasSearch,
hasFilter,
networkFilter,
match,
curOrderOpt,
])
}, [selectedConnections, isTableLayout, hasSearch, match, curOrderOpt])
const displayRows = useConnectionRowViews(
isTableLayout ? EMPTY_CONNECTIONS : filterConn,
@@ -274,24 +255,6 @@ const ConnectionsPage = () => {
))}
</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
sx={{
flex: 1,
+11 -8
View File
@@ -39,18 +39,21 @@ const LogPage = () => {
return []
}
const hasSearch = searchState.text.length > 0
if (!hasSearch && logState === 'all') return logData
// Server-side filtering handles level filtering via query parameters
// We only need to apply search filtering here
return logData.filter((data) => {
if (logState !== 'all' && !data.type.includes(logState)) return false
if (!hasSearch) return true
// 构建完整的搜索文本,包含时间、类型和内容
const searchText =
`${data.time || ''} ${data.type} ${data.payload}`.toLowerCase()
return match(searchText)
const matchesSearch = match(searchText)
return (
(logState == 'all' ? true : data.type.includes(logState)) &&
matchesSearch
)
})
}, [logData, logState, match, searchState.text])
}, [logData, logState, match])
const filteredLogs = useMemo(
() => (isDescending ? [...filterLogs].reverse() : filterLogs),
+13 -6
View File
@@ -1,22 +1,29 @@
import path from 'node:path'
import legacy from '@vitejs/plugin-legacy'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import svgr from 'vite-plugin-svgr'
import { guardedLegacy } from './scripts/modern-polyfills.mjs'
const svgPlugin = svgr()
const svgLoad = svgPlugin.load
if (!svgLoad) throw new Error('vite-plugin-svgr must provide a load hook')
// SVGR's internal include filter still crosses the JS bridge for every module.
svgPlugin.load = {
filter: { id: /\.svg\?react$/ },
handler: typeof svgLoad === 'function' ? svgLoad : svgLoad.handler,
}
export default defineConfig({
root: 'src',
server: { port: 3000 },
plugins: [
svgr(),
svgPlugin,
react(),
legacy({
guardedLegacy({
modernTargets: ['edge>=109', 'safari>=14'],
renderLegacyChunks: false,
// Auto-detected from the emitted bundle, dependencies included,
// so post-Safari-14 APIs never depend on extending a manual list.
modernPolyfills: true,
additionalModernPolyfills: [
path.resolve('./src/polyfills/matchMedia.js'),
path.resolve('./src/polyfills/WeakRef.js'),