Merge remote-tracking branch 'origin/dev' into fix/grouped-virtual-list

This commit is contained in:
oomeow
2026-09-16 19:23:02 +08:00
12 changed files with 990 additions and 1099 deletions
+8
View File
@@ -30,11 +30,19 @@ jobs:
PR_BODY: ${{ github.event.pull_request.body }}
LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
DRAFT: ${{ github.event.pull_request.draft }}
ASSOCIATION: ${{ github.event.pull_request.author_association }}
AUTHOR_TYPE: ${{ github.event.pull_request.user.type }}
run: |
if [ "$DRAFT" = "true" ] || printf '%s' "$LABELS" | grep -q 'no-issue-needed'; then
echo "Draft or no-issue-needed; linkage check skipped."
exit 0
fi
# OWNER/MEMBER/COLLABORATOR and bots mirror the slop review's
# skip-roles/skip-bots trust tier.
if [ "$AUTHOR_TYPE" = "Bot" ] || [ "$ASSOCIATION" = "OWNER" ] || [ "$ASSOCIATION" = "MEMBER" ] || [ "$ASSOCIATION" = "COLLABORATOR" ]; then
echo "Bot or trusted author ($ASSOCIATION); linkage check skipped."
exit 0
fi
if printf '%s\n%s\n' "$PR_TITLE" "$PR_BODY" | grep -Eq '#[0-9]+|issues/[0-9]+'; then
echo "Issue reference found."
else
Generated
+452 -591
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -61,10 +61,10 @@ clash-verge-i18n = { path = "crates/clash-verge-i18n" }
clash-verge-limiter = { path = "crates/clash-verge-limiter" }
tracing-estuary = { git = "https://github.com/Tunglies/tracing-estuary.git", branch = "main" }
tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sysinfo" }
tracing = "0.1"
tracing = "0.1.44"
tauri = { version = "2.11.5" }
tauri-plugin-clipboard-manager = "2.3.2"
tauri-plugin-clipboard-manager = "2.3.3"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.104"
chrono = "0.4.45"
@@ -76,9 +76,9 @@ tokio = { version = "1.53.1", features = [
"sync",
] }
flexi_logger = "0.31.10"
log = "0.4.33"
log = "0.4.34"
regex = "1.13.1"
reqwest = { version = "0.13.4", features = [
reqwest = { version = "0.13.5", features = [
"json",
"cookies",
"gzip",
@@ -92,7 +92,7 @@ smartstring = { version = "1.0.1" }
serde = { version = "1.0.229" }
serde_json = { version = "1.0.151" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.13.1" }
bitflags = { version = "2.13.2" }
socket2 = "0.6.5"
# *** For Windows platform only ***
+1
View File
@@ -27,6 +27,7 @@
**🖥️ Windows**
- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题
- 修复 Windows 系统代理设置偶发失败的问题
- 修复 Windows 关机被应用阻止的问题
- 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加
+1 -1
View File
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
rust-i18n = "4.2.1"
rust-i18n = "4.2.2"
sys-locale = "0.3.2"
[lints]
+14 -14
View File
@@ -56,13 +56,13 @@
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "~2.5.9",
"@tauri-apps/plugin-opener": "2.5.4",
"@tauri-apps/plugin-updater": "2.10.1",
"@tauri-apps/plugin-http": "~2.6.0",
"@tauri-apps/plugin-opener": "2.5.5",
"@tauri-apps/plugin-updater": "2.11.0",
"ahooks": "^3.9.7",
"cidr-block": "^2.3.0",
"compare-versions": "^6.1.1",
"dayjs": "1.11.21",
"dayjs": "1.11.23",
"foxact": "^0.3.4",
"foxts": "^5.4.2",
"i18next": "^26.2.0",
@@ -73,11 +73,11 @@
"monaco-yaml": "^5.5.0",
"nanoid": "^6.0.0",
"qrcode.react": "^4.2.0",
"react": "19.2.8",
"react-dom": "19.2.8",
"react-error-boundary": "6.1.2",
"react": "19.3.0",
"react-dom": "19.3.0",
"react-error-boundary": "6.1.5",
"react-hook-form": "^7.76.1",
"react-i18next": "17.0.11",
"react-i18next": "17.0.14",
"react-markdown": "10.1.0",
"react-router": "^8.0.0",
"rehype-raw": "^7.0.0",
@@ -96,12 +96,12 @@
"@tauri-apps/cli": "2.11.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.12.4",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.4",
"@types/react": "19.3.0",
"@types/react-dom": "19.3.0",
"@types/validator": "^13.15.10",
"@vitejs/plugin-legacy": "^8.0.2",
"@vitejs/plugin-react": "^6.0.2",
"@wdio/tauri-service": "1.3.0",
"@wdio/tauri-service": "1.4.0",
"adm-zip": "^0.6.0",
"axios": "^1.16.1",
"cli-color": "^2.0.4",
@@ -121,7 +121,7 @@
"https-proxy-agent": "^9.0.0",
"husky": "^9.1.7",
"jiti": "^2.7.0",
"knip": "6.32.1",
"knip": "6.35.1",
"lint-staged": "^17.0.5",
"sass": "^1.100.0",
"tar": "^7.5.15",
@@ -131,8 +131,8 @@
"vite": "^8.0.14",
"vite-plugin-svgr": "^5.2.0",
"vitest": "^5.0.0",
"webdriverio": "9.30.0",
"ws": "8.21.0"
"webdriverio": "9.31.9",
"ws": "8.21.3"
},
"lint-staged": {
"*.{ts,tsx,js,mjs}": [
+369 -464
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -59,13 +59,13 @@ smartstring = { workspace = true, features = ["serde"] }
bitflags = { workspace = true }
socket2 = { workspace = true }
warp = { version = "0.4.3", features = ["server"] }
open = "5.4.1"
open = "5.4.4"
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
dirs = "7.0.0"
dunce = "1.0.5"
nanoid = "0.5"
chrono = { workspace = true }
boa_engine = "0.21.1"
boa_engine = "0.22.0"
once_cell = { version = "1.21.4", features = ["parking_lot"] }
percent-encoding = "2.3.2"
reqwest = { workspace = true }
@@ -74,37 +74,37 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", branch
"guard",
] }
network-interface = { version = "2.0.5", features = ["serde"] }
tauri-plugin-shell = "2.3.5"
tauri-plugin-opener = "2.5.4"
tauri-plugin-dialog = "2.7.2"
tauri-plugin-fs = "2.5.1"
tauri-plugin-shell = "2.3.6"
tauri-plugin-opener = "2.5.5"
tauri-plugin-dialog = "2.7.3"
tauri-plugin-fs = "2.5.2"
tauri-plugin-process = "2.3.1"
tauri-plugin-deep-link = "2.4.9"
tauri-plugin-deep-link = "2.4.10"
tauri-plugin-window-state = "2.4.1"
zip = "8.6.0"
flate2 = "1.1.9"
flate2 = "1.1.10"
reqwest_dav = "0.3.3"
aes-gcm = { version = "0.11.0" }
aes-gcm = { version = "0.11.1" }
base64 = "0.23.1"
sha2 = "0.11"
getrandom = "0.4.3"
futures = "0.3.34"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.3"
tauri-plugin-notification = "2.4.0"
tokio-stream = "0.1.19"
backon = { version = "1.6.0", features = ["tokio-sleep"] }
tauri-plugin-http = "2.5.9"
tauri-plugin-http = "2.6.0"
tracing = { workspace = true }
tauri-plugin-devtools = { version = "2.1.0", optional = true }
tauri-plugin-devtools = { version = "2.2.1", optional = true }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
async-trait = "0.1.92"
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.0", version = "2.7.0", features = [
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.1", version = "2.7.1", features = [
"client",
] }
arc-swap = "1.9.2"
tokio-rustls = "0.26.4"
rustls = { version = "0.23.43", features = ["ring"] }
tokio-rustls = "0.26.5"
rustls = { version = "0.23.45", features = ["ring"] }
webpki-roots = "1.0.9"
dark-light = "3.0.0"
bytes = "1.12.1"
@@ -114,7 +114,7 @@ tokio-util = { version = "0.7.19", features = ["time"] }
libc = "0.2.189"
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-wdio-webdriver = { version = "=1.3.0", optional = true }
tauri-plugin-wdio-webdriver = { version = "=1.4.0", optional = true }
objc2 = "0.6.4"
objc2-foundation = { version = "0.3.2", features = [
"NSString",
@@ -148,7 +148,7 @@ windows-sys.workspace = true
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.2"
tauri-plugin-updater = "2.10.1"
tauri-plugin-updater = "2.11.0"
[dev-dependencies]
criterion = { workspace = true }
+113 -3
View File
@@ -564,8 +564,39 @@ fn uninstall_service() -> Result<()> {
#[cfg(target_os = "windows")]
fn install_service() -> Result<()> {
use std::process::Output;
logging!(info, Type::Service, "install service");
let cores = crate::config::IVerge::VALID_CLASH_CORES
.iter()
.map(|core| service_core_path(core, ".exe"))
.collect::<Result<Vec<_>>>()?;
install_windows_service_with_cores(&cores, run_windows_service_installer)
}
#[cfg(target_os = "windows")]
fn install_windows_service_with_cores(
cores: &[PathBuf],
mut run_installer: impl FnMut(&[std::ffi::OsString]) -> Result<()>,
) -> Result<()> {
let cores: Vec<_> = cores.iter().filter(|core| core.is_file()).collect();
if cores.is_empty() {
bail!("no core executable is available; restore a bundled core before installing the service");
}
let mut arguments = vec!["--install-service".into()];
for core in cores {
let digest = sha256_hex(core)?;
arguments.extend([
"--install-core".into(),
core.as_os_str().to_owned(),
"--sha256".into(),
digest.into(),
]);
}
run_installer(&arguments).context("failed to install the service and its cores; choose Repair to retry")
}
#[cfg(target_os = "windows")]
fn run_windows_service_installer(arguments: &[std::ffi::OsString]) -> Result<()> {
use std::process::Output;
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
@@ -583,7 +614,7 @@ fn install_service() -> Result<()> {
let level = token.privilege_level()?;
let output = match level {
PrivilegeLevel::NotPrivileged => {
let status = RunasCommand::new(&install_path).show(false).status()?;
let status = RunasCommand::new(&install_path).args(arguments).show(false).status()?;
Output {
status,
stdout: Vec::new(),
@@ -592,7 +623,10 @@ fn install_service() -> Result<()> {
}
_ => {
// StdCommand returns Output directly
StdCommand::new(&install_path).creation_flags(0x08000000).output()?
StdCommand::new(&install_path)
.args(arguments)
.creation_flags(0x08000000)
.output()?
}
};
@@ -2167,6 +2201,82 @@ mod tests {
}
}
#[cfg(target_os = "windows")]
#[test]
fn windows_service_install_combines_service_and_attested_cores() -> anyhow::Result<()> {
use std::ffi::OsString;
let root = TestDirectory::new("scoop install")?;
let cores: Vec<_> = crate::config::IVerge::VALID_CLASH_CORES
.iter()
.map(|name| root.path().join(format!("{name}.exe")))
.collect();
for core in &cores {
std::fs::write(core, b"abc")?;
}
let mut calls = Vec::new();
super::install_windows_service_with_cores(&cores, |arguments| {
calls.push(arguments.to_vec());
Ok(())
})?;
assert_eq!(calls.len(), 1, "service and cores must share one elevation");
let digest = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
let mut expected: Vec<OsString> = vec!["--install-service".into()];
expected.extend(cores.iter().flat_map(|core| {
[
"--install-core".into(),
core.as_os_str().to_owned(),
"--sha256".into(),
digest.into(),
]
}));
assert_eq!(calls[0], expected);
Ok(())
}
#[cfg(target_os = "windows")]
#[test]
fn windows_service_install_skips_unavailable_cores() -> anyhow::Result<()> {
let root = TestDirectory::new("missing-core")?;
let core = root.path().join("verge-mihomo.exe");
let missing = root.path().join("verge-mihomo-alpha.exe");
std::fs::write(&core, b"abc")?;
let cores = [core.clone(), missing.clone(), root.path().to_path_buf()];
let mut calls = Vec::new();
super::install_windows_service_with_cores(&cores, |arguments| {
calls.push(arguments.to_vec());
Ok(())
})?;
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].len(), 5);
assert_eq!(calls[0][0], "--install-service");
assert_eq!(calls[0][2], core.as_os_str());
let error = super::install_windows_service_with_cores(&[missing, root.path().to_path_buf()], |_| {
panic!("no available core must fail before invoking the installer")
})
.expect_err("installation requires at least one available core");
assert!(error.to_string().contains("no core executable"));
Ok(())
}
#[cfg(target_os = "windows")]
#[test]
fn windows_service_install_propagates_installer_failure() -> anyhow::Result<()> {
let root = TestDirectory::new("install-failure")?;
let core = root.path().join("verge-mihomo.exe");
std::fs::write(&core, b"abc")?;
let mut calls = 0;
let result = super::install_windows_service_with_cores(std::slice::from_ref(&core), |_| {
calls += 1;
bail!("installer failed with exit code 1")
});
let error = result.expect_err("installation failure must not be reported as success");
assert!(format!("{error:#}").contains("exit code 1"));
assert!(error.to_string().contains("Repair"));
assert_eq!(calls, 1);
Ok(())
}
fn staging_directory(home: &Path) -> PathBuf {
home.join("Applications/.clash-verge-rev-dev/service-core")
}
+2 -1
View File
@@ -108,7 +108,8 @@ export const SystemInfoCard = () => {
goToSettings()
}
} catch (err) {
showNotice.error(err)
console.warn('update check failed:', err)
showNotice.warning(err)
}
})
@@ -54,8 +54,9 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
} else {
updateRef.current?.open()
}
} catch (err: any) {
showNotice.error(err)
} catch (err) {
console.warn('update check failed:', err)
showNotice.warning(err)
}
}
+5 -1
View File
@@ -4,7 +4,7 @@ import { ReactNode, isValidElement } from 'react'
import type { FailedOperation } from '@/services/cmds'
import type { TranslationKey } from '@/types/generated/i18n-keys'
type NoticeType = 'success' | 'error' | 'info'
type NoticeType = 'success' | 'error' | 'info' | 'warning'
interface NoticeTranslationDescriptor {
key: string
@@ -47,6 +47,7 @@ type ShowNotice = ((
success: NoticeShortcut
error: NoticeShortcut
info: NoticeShortcut
warning: NoticeShortcut
}
type NoticeSubscriber = () => void
@@ -54,6 +55,7 @@ type NoticeSubscriber = () => void
const DEFAULT_DURATIONS: Readonly<Record<NoticeType, number>> = {
success: 3000,
info: 5000,
warning: 6000,
error: 8000,
}
@@ -509,6 +511,8 @@ export const showNotice: ShowNotice = Object.assign(baseShowNotice, {
baseShowNotice('error', message, ...extras),
info: (message: NoticeContent, ...extras: NoticeExtra[]) =>
baseShowNotice('info', message, ...extras),
warning: (message: NoticeContent, ...extras: NoticeExtra[]) =>
baseShowNotice('warning', message, ...extras),
})
export function hideNotice(id: number) {