refactor(ipc): generate bindings with Specta

This commit is contained in:
Tunglies
2026-10-08 01:57:25 +08:00
parent 1c2817023e
commit 2e439b5e1a
71 changed files with 2091 additions and 1087 deletions
+7 -1
View File
@@ -102,6 +102,12 @@ jobs:
filters: |
contract:
- 'src/services/contract.ts'
- 'src/services/bindings.ts'
- 'Cargo.lock'
- 'Cargo.toml'
- 'src-tauri/Cargo.toml'
- 'crates/**'
- 'src-tauri/src/**'
- 'scripts/gen-contract.mjs'
- 'src-tauri/src/core/notify/**'
- 'src-tauri/src/bin/**'
@@ -163,4 +169,4 @@ jobs:
if: steps.check_inputs.outputs.contract == 'true'
run: |
pnpm gen:contract
git diff --exit-code -- src/services/contract.ts
git diff --exit-code -- src/services/bindings.ts
Generated
+95
View File
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "Inflector"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "adler2"
version = "2.0.1"
@@ -1135,6 +1141,8 @@ dependencies = [
"sha2 0.11.0",
"smartstring",
"socket2",
"specta",
"specta-typescript",
"sysproxy",
"tauri",
"tauri-build",
@@ -1155,6 +1163,7 @@ dependencies = [
"tauri-plugin-updater",
"tauri-plugin-wdio-webdriver",
"tauri-plugin-window-state",
"tauri-specta",
"tokio",
"tokio-rustls",
"tokio-stream",
@@ -1213,6 +1222,7 @@ dependencies = [
"rust_iso3166",
"serde",
"serde_json",
"specta",
"tokio",
]
@@ -6805,6 +6815,58 @@ dependencies = [
"system-deps 6.2.2",
]
[[package]]
name = "specta"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f9a30cbcbb7011f1da7d73483983bf838af123883e45f2b36ed76328df9c50"
dependencies = [
"paste",
"rustc_version 0.4.1",
"serde_json",
"specta-macros",
]
[[package]]
name = "specta-macros"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ce14957ecc2897f1f848b8255b6531d13ddf49cbcf506b7c2c9fb1d005593bb"
dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "specta-serde"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8a72b755ddb8949fd8f17c5db43f0e8a806ea587d9bc602ee3f73240c00029"
dependencies = [
"specta",
"specta-macros",
]
[[package]]
name = "specta-typescript"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "639404ee95557f2f8b7e4cb773ffefd45304c7ab8ba21ac83b69051595e083c0"
dependencies = [
"specta",
]
[[package]]
name = "specta-util"
version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29b1fc02b446f7244a92924fe68c0555921209f1d342990cd1539e9138e69502"
dependencies = [
"specta",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -7160,6 +7222,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serialize-to-javascript",
"specta",
"swift-rs",
"tauri-build",
"tauri-macros",
@@ -7277,6 +7340,7 @@ dependencies = [
"libc",
"parking_lot",
"serde",
"specta",
"sysinfo",
"tauri",
"tauri-plugin-clipboard-manager",
@@ -7656,6 +7720,37 @@ dependencies = [
"wry",
]
[[package]]
name = "tauri-specta"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee080f36d2ac17ce2f3a82fb53f02d664e8345457de51b56dad3c394dacc41a2"
dependencies = [
"heck 0.5.0",
"serde",
"serde_json",
"specta",
"specta-serde",
"specta-typescript",
"specta-util",
"tauri",
"tauri-specta-macros",
"thiserror 2.0.20",
]
[[package]]
name = "tauri-specta-macros"
version = "2.0.0-rc.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a59dfdce06c98d8d211619bea5fdb39486d8a8c558e12b2d2ce255972320012"
dependencies = [
"darling",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "tauri-utils"
version = "2.10.1"
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
clash-verge-logging = { workspace = true }
chrono = { workspace = true }
log = { workspace = true }
+1 -1
View File
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
use super::utils::{country_code_to_emoji, get_local_date_string};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
pub struct UnlockItem {
pub name: String,
pub status: String,
@@ -5,6 +5,7 @@ edition = "2024"
rust-version = "1.91"
[dependencies]
specta = { version = "=2.0.0-rc.25", features = ["derive", "function"] }
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
@@ -1,11 +1,11 @@
use parking_lot::RwLock;
use serde::Serialize;
use tauri::{AppHandle, Runtime, State, command};
use tauri_plugin_clipboard_manager::{ClipboardExt as _, Error};
use tauri_plugin_clipboard_manager::ClipboardExt as _;
use crate::Platform;
#[derive(Serialize)]
#[derive(Serialize, specta::Type)]
pub struct SystemInfo {
pub system_name: String,
pub system_version: String,
@@ -31,23 +31,26 @@ impl From<Platform> for SystemInfo {
}
#[command]
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, Error> {
#[specta::specta]
pub fn get_system_info(state: State<'_, RwLock<Platform>>) -> Result<SystemInfo, String> {
let platform = state.inner().read();
Ok(SystemInfo::from(platform.clone()))
}
/// 获取应用的运行时间(毫秒)
#[command]
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, Error> {
#[specta::specta]
pub fn get_app_uptime(state: State<'_, RwLock<Platform>>) -> Result<u128, String> {
Ok(state.inner().read().appinfo.app_startup_time.elapsed().as_millis())
}
#[command]
#[specta::specta]
pub fn export_diagnostic_info<R: Runtime>(
app_handle: AppHandle<R>,
state: State<'_, RwLock<Platform>>,
) -> Result<(), Error> {
) -> Result<(), String> {
let info = state.inner().read().to_string();
let clipboard = app_handle.clipboard();
clipboard.write_text(info)
clipboard.write_text(info).map_err(|error| error.to_string())
}
+1
View File
@@ -18,6 +18,7 @@
"ignoreDependencies": ["lint-staged"],
"ignoreIssues": {
"src/types/generated/**": ["exports", "types"],
"src/services/bindings.ts": ["exports", "types"],
"src/services/contract.ts": ["exports", "types"]
},
"cycles": {
+15 -84
View File
@@ -1,91 +1,22 @@
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
/**
* Regenerates src/services/contract.ts from the backend wire contract.
* Source of truth: `cargo run -p clash-verge --bin frontend-contract` inside
* src-tauri, which serializes the same enum variants and event payloads the
* app emits at runtime. CI reruns this and fails on a non-empty diff.
*/
const ROOT = path.resolve(import.meta.dirname, '..')
const OUT = path.join(ROOT, 'src/services/contract.ts')
const contract = JSON.parse(
execFileSync(
'cargo',
['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '-q'],
{
cwd: path.join(ROOT, 'src-tauri'),
encoding: 'utf8',
maxBuffer: 16 * 1024 * 1024,
},
),
)
if (contract.version !== 1) {
throw new Error(`unsupported wire contract version: ${contract.version}`)
}
/** TS type per event, derived from the serialized sample payload's JSON shape. */
const payloadType = (name, sample) => {
if (sample === null) return 'null'
if (typeof sample === 'string') return 'string'
if (Array.isArray(sample)) {
if (name === 'verge://notice-message') return '[NoticeStatus, string]'
throw new Error(`unexpected array payload for ${name}`)
}
if (typeof sample === 'object') {
const keys = Object.keys(sample)
if (keys.length === 1 && keys[0] === 'uid') return '{ uid: string }'
if (name === 'verge://run-state-changed') return 'RunState'
throw new Error(`unexpected object payload for ${name}: ${keys.join(',')}`)
}
throw new Error(`unexpected payload for ${name}`)
}
const needsRunState = contract.events.some(
(e) => e.name === 'verge://run-state-changed',
)
if (!needsRunState)
throw new Error('run-state-changed event missing from contract')
const lines = []
lines.push(
'// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.',
)
lines.push('// Regen: pnpm gen:contract')
lines.push('')
lines.push("import type { RunState } from './cmds'")
lines.push('')
lines.push(
'/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */',
)
lines.push('export type NoticeStatus =')
for (const status of contract.noticeStatuses) {
lines.push(` | ${JSON.stringify(status)}`)
}
lines.push('')
lines.push('/** Event name → payload type for every backend-emitted event. */')
lines.push('export interface VergeEventPayloads {')
for (const event of contract.events) {
const key = JSON.stringify(event.name)
lines.push(` ${key}: ${payloadType(event.name, event.sample)}`)
}
lines.push('}')
lines.push('')
const content = `${lines.join('\n')}\n`
fs.writeFileSync(OUT, content)
// Match the repo's biome style so CI parity (`git diff --exit-code`) is stable.
execFileSync(
process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
['exec', 'biome', 'format', '--write', path.relative(ROOT, OUT)],
{ cwd: ROOT, stdio: 'ignore' },
'cargo',
['run', '-p', 'clash-verge', '--bin', 'frontend-contract', '--locked', '-q'],
{
cwd: ROOT,
stdio: 'inherit',
},
)
console.log(
`[gen-contract] wrote ${path.relative(ROOT, OUT)}: ${contract.events.length} events, ${contract.noticeStatuses.length} statuses`,
execFileSync(
process.execPath,
[
path.join(ROOT, 'node_modules/@biomejs/biome/bin/biome'),
'format',
'--write',
'src/services/bindings.ts',
],
{ cwd: ROOT, stdio: 'inherit' },
)
+3
View File
@@ -109,6 +109,9 @@ webpki-roots = "1.0.9"
dark-light = "3.0.0"
bytes = "1.12.1"
tokio-util = { version = "0.7.19", features = ["time"] }
specta = { version = "=2.0.0-rc.25", features = ["derive", "function", "serde_json"] }
tauri-specta = { version = "=2.0.0-rc.25", features = ["derive", "typescript"] }
specta-typescript = "=0.0.12"
[target.'cfg(unix)'.dependencies]
libc = "0.2.190"
+6 -7
View File
@@ -1,8 +1,7 @@
//! Prints the frontend wire contract as JSON to stdout. Zero new dependencies:
//! the payload is derived from the same serializers the app uses at runtime.
//! Consumed by scripts/gen-contract.mjs; CI regenerates and diffs the committed
//! TypeScript artifact.
fn main() {
println!("{}", app_lib::frontend_wire_contract());
fn main() -> Result<(), Box<dyn std::error::Error>> {
app_lib::ipc::builder().export(
specta_typescript::Typescript::default(),
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/services/bindings.ts"),
)?;
Ok(())
}
+24 -6
View File
@@ -9,12 +9,14 @@ use smartstring::alias::String;
use tauri::{AppHandle, Manager as _, ipc::Channel};
#[tauri::command]
#[specta::specta]
pub async fn open_app_dir() -> CmdResult<()> {
let app_dir = dirs::app_home_dir().stringify_err()?;
open::that(app_dir).stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn open_core_dir() -> CmdResult<()> {
let core_dir = tauri::utils::platform::current_exe().stringify_err()?;
let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
@@ -22,12 +24,14 @@ pub async fn open_core_dir() -> CmdResult<()> {
}
#[tauri::command]
#[specta::specta]
pub async fn open_logs_dir() -> CmdResult<()> {
let log_dir = dirs::app_logs_dir().stringify_err()?;
open::that(log_dir).stringify_err()
}
#[tauri::command]
#[specta::specta]
pub fn open_devtools(app_handle: AppHandle) {
if let Some(window) = app_handle.get_webview_window("main") {
if !window.is_devtools_open() {
@@ -39,22 +43,26 @@ pub fn open_devtools(app_handle: AppHandle) {
}
#[tauri::command]
#[specta::specta]
pub async fn exit_app() {
feat::quit().await;
}
#[tauri::command]
#[specta::specta]
pub async fn restart_app() -> CmdResult<()> {
feat::restart_app().await;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn install_update(
app_handle: AppHandle,
version: String,
version: std::string::String,
on_event: Channel<DownloadEvent>,
) -> CmdResult<bool> {
let version: String = version.into();
SilentUpdater::global()
.install_update(&app_handle, &version, |event| {
let _ = on_event.send(event);
@@ -64,22 +72,32 @@ pub async fn install_update(
}
#[tauri::command]
#[specta::specta]
pub fn cancel_update_download() {
SilentUpdater::global().cancel_download();
}
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
#[specta::specta]
pub fn get_app_dir() -> CmdResult<std::string::String> {
let app_home_dir = dirs::app_home_dir().stringify_err()?.to_string_lossy().into();
Ok(app_home_dir)
}
#[tauri::command]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
feat::download_icon_cache(url, name).await
#[specta::specta]
pub async fn download_icon_cache(
url: std::string::String,
name: std::string::String,
) -> CmdResult<std::string::String> {
let url: String = url.into();
let name: String = name.into();
feat::download_icon_cache(url, name).await.map(Into::into)
}
#[tauri::command]
pub async fn copy_icon_file(path: String, icon_info: feat::IconInfo) -> CmdResult<String> {
feat::copy_icon_file(path, icon_info).await
#[specta::specta]
pub async fn copy_icon_file(path: std::string::String, icon_info: feat::IconInfo) -> CmdResult<std::string::String> {
let path: String = path.into();
feat::copy_icon_file(path, icon_info).await.map(Into::into)
}
+16 -5
View File
@@ -4,33 +4,44 @@ use feat::LocalBackupFile;
use smartstring::alias::String;
#[tauri::command]
#[specta::specta]
pub async fn create_local_backup() -> CmdResult<()> {
feat::create_local_backup().await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn list_local_backup() -> CmdResult<Vec<LocalBackupFile>> {
feat::list_local_backup().await.stringify_err()
}
#[tauri::command]
pub async fn delete_local_backup(filename: String) -> CmdResult<()> {
#[specta::specta]
pub async fn delete_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::delete_local_backup(filename).await.stringify_err()
}
#[tauri::command]
pub async fn restore_local_backup(filename: String) -> CmdResult<()> {
#[specta::specta]
pub async fn restore_local_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::restore_local_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
}
#[tauri::command]
pub async fn import_local_backup(source: String) -> CmdResult<String> {
feat::import_local_backup(source).await.stringify_err()
#[specta::specta]
pub async fn import_local_backup(source: std::string::String) -> CmdResult<std::string::String> {
let source: String = source.into();
feat::import_local_backup(source).await.map(Into::into).stringify_err()
}
#[tauri::command]
pub async fn export_local_backup(filename: String, destination: String) -> CmdResult<()> {
#[specta::specta]
pub async fn export_local_backup(filename: std::string::String, destination: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
let destination: String = destination.into();
feat::export_local_backup(filename, destination).await.stringify_err()
}
+34 -14
View File
@@ -12,30 +12,34 @@ use crate::{
},
};
use clash_verge_logging::{Type, logging, logging_error};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use tokio::fs;
#[tauri::command]
#[specta::specta]
pub async fn copy_clash_env() -> CmdResult {
feat::copy_clash_env().await;
Ok(())
}
#[tauri::command]
#[specta::specta]
pub async fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().await.data_arc().get_client_info())
}
#[tauri::command]
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
feat::patch_clash(&payload)
#[specta::specta]
pub async fn patch_clash_config(payload: crate::ipc::YamlMapping) -> CmdResult {
feat::patch_clash(&payload.0)
.await
.map_err(|error| proxy_aware_coded_error(&error, "CLASH_CONFIG_UPDATE_FAILED"))
}
#[tauri::command]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
#[specta::specta]
pub async fn patch_clash_mode(payload: std::string::String) -> CmdResult {
let payload: String = payload.into();
feat::change_clash_mode(payload)
.await
.with_error_code("CLASH_MODE_UPDATE_FAILED")
@@ -43,12 +47,15 @@ pub async fn patch_clash_mode(payload: String) -> CmdResult {
/// Reads the saved mode without depending on strict mihomo `/configs` deserialization.
#[tauri::command]
pub async fn get_clash_mode() -> CmdResult<Option<String>> {
Ok(Config::clash().await.data_arc().get_mode().map(Into::into))
#[specta::specta]
pub async fn get_clash_mode() -> CmdResult<Option<std::string::String>> {
Ok(Config::clash().await.data_arc().get_mode())
}
#[tauri::command]
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFailure>> {
#[specta::specta]
pub async fn change_clash_core(clash_core: std::string::String) -> CmdResult<Option<CommandFailure>> {
let clash_core: String = clash_core.into();
logging!(info, Type::Config, "changing core to {clash_core}");
match CoreManager::global().change_core(&clash_core).await {
@@ -80,6 +87,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<CommandFa
}
#[tauri::command]
#[specta::specta]
pub async fn restart_core() -> CmdResult {
logging_error!(Type::Core, profiles_save_file_safe().await);
let result = CoreManager::global()
@@ -94,6 +102,7 @@ pub async fn restart_core() -> CmdResult {
/// Replaces the managed core binary atomically instead of letting mihomo overwrite itself.
#[tauri::command]
#[specta::specta]
pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeReport> {
let report = feat::upgrade_core(force).await.with_error_code("CORE_UPGRADE_FAILED")?;
if report.upgraded {
@@ -103,7 +112,9 @@ pub async fn upgrade_clash_core(force: bool) -> CmdResult<feat::CoreUpgradeRepor
}
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
#[specta::specta]
pub async fn test_delay(url: std::string::String) -> CmdResult<u32> {
let url: String = url.into();
let result = match feat::test_delay(url).await {
Ok(delay) => delay,
Err(e) => {
@@ -115,13 +126,14 @@ pub async fn test_delay(url: String) -> CmdResult<u32> {
}
#[tauri::command]
pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
#[specta::specta]
pub async fn save_dns_config(dns_config: crate::ipc::YamlMapping) -> CmdResult {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?;
let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config.0).stringify_err()?;
fs::write(&dns_path, yaml_str).await.stringify_err()?;
logging!(info, Type::Config, "DNS config saved to {dns_path:?}");
@@ -129,22 +141,27 @@ pub async fn save_dns_config(dns_config: Mapping) -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub fn take_dns_override_notice() -> bool {
crate::config::dns::take_dns_override_notice()
}
#[tauri::command]
#[specta::specta]
pub async fn set_dns_override(
profile_uid: String,
profile_uid: std::string::String,
enabled: bool,
confirmation: Option<String>,
confirmation: Option<std::string::String>,
) -> CmdResult<feat::DnsOverrideOutcome> {
let profile_uid: String = profile_uid.into();
let confirmation = confirmation.map(String::from);
feat::set_dns_override(profile_uid, enabled, confirmation)
.await
.map_err(|error| proxy_aware_coded_error(&error, "DNS_OVERRIDE_UPDATE_FAILED"))
}
#[tauri::command]
#[specta::specta]
pub async fn apply_dns_config(apply: bool) -> CmdResult {
if apply {
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
@@ -202,20 +219,22 @@ pub async fn apply_dns_config(apply: bool) -> CmdResult {
}
#[tauri::command]
pub async fn get_dns_config_content() -> CmdResult<Option<String>> {
#[specta::specta]
pub async fn get_dns_config_content() -> CmdResult<Option<std::string::String>> {
use crate::utils::dirs;
use tokio::fs;
let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG);
match fs::read_to_string(&dns_path).await {
Ok(content) => Ok(Some(content.into())),
Ok(content) => Ok(Some(content)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(CommandFailure::plain(error)),
}
}
#[tauri::command]
#[specta::specta]
pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
let app_dir = dirs::app_home_dir().stringify_err()?;
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
@@ -232,6 +251,7 @@ pub async fn validate_dns_config() -> CmdResult<ValidationOutcome> {
}
#[tauri::command]
#[specta::specta]
pub async fn get_clash_logs() -> CmdResult<Vec<std::string::String>> {
let logs = CoreManager::global().get_clash_logs().await.unwrap_or_default();
Ok(logs)
+1
View File
@@ -3,6 +3,7 @@ use crate::core::lightweight;
use super::CmdResult;
#[tauri::command]
#[specta::specta]
pub async fn entry_lightweight_mode() -> CmdResult {
lightweight::entry_lightweight_mode().await;
Ok(())
+2
View File
@@ -5,11 +5,13 @@ use crate::{
};
#[tauri::command]
#[specta::specta]
pub async fn probe_listener(request: ListenerProbe) -> CmdResult<ListenerProbeOutcome> {
feat::probe_listener(request).await.stringify_err()
}
#[tauri::command]
#[specta::specta]
pub async fn save_proxy_ports(settings: ProxyPortSettings) -> CmdResult<SaveProxyPortsOutcome> {
feat::save_proxy_ports(settings)
.await
@@ -4,6 +4,7 @@ use std::time::Duration;
use tauri::{command, ipc::Channel};
#[command]
#[specta::specta]
pub fn get_unlock_items() -> Vec<UnlockItem> {
clash_verge_media_unlock::default_unlock_items()
}
@@ -26,6 +27,7 @@ async fn create_client() -> Result<Client, String> {
}
#[command]
#[specta::specta]
pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<UnlockItem>, String> {
let client = create_client().await?;
Ok(clash_verge_media_unlock::check_media_unlock(&client, |item| {
@@ -35,6 +37,7 @@ pub async fn check_media_unlock(on_complete: Channel<UnlockItem>) -> Result<Vec<
}
#[command]
#[specta::specta]
pub async fn check_media_unlock_item(name: String) -> Result<UnlockItem, String> {
let client = create_client().await?;
clash_verge_media_unlock::check_media_unlock_item(&client, &name).await
+1 -1
View File
@@ -4,7 +4,7 @@ use smartstring::alias::String;
pub type CmdResult<T = ()> = Result<T, CommandFailure>;
/// Structured command failure returned to the frontend.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
pub struct CommandFailure {
/// Stable frontend identifier, when classified.
#[serde(skip_serializing_if = "Option::is_none")]
+32 -15
View File
@@ -3,13 +3,12 @@ use crate::cmd::StringifyErr as _;
use crate::core::{proxy_control, sysopt::Sysopt};
use gethostname::gethostname;
use network_interface::NetworkInterface;
use serde_yaml_ng::Mapping;
use sysproxy::{Autoproxy, Sysproxy};
use tauri_plugin_clash_verge_sysinfo;
#[tauri::command]
pub async fn get_sys_proxy() -> CmdResult<Mapping> {
#[specta::specta]
pub async fn get_sys_proxy() -> CmdResult<SystemProxy> {
Sysopt::global().wait_idle().await;
// With no network service there is no proxy configured anywhere, which reads as disabled.
let sys_proxy = match Sysproxy::get_system_proxy() {
@@ -23,15 +22,16 @@ pub async fn get_sys_proxy() -> CmdResult<Mapping> {
ref enable,
} = sys_proxy;
let mut map = Mapping::new();
map.insert("enable".into(), (*enable).into());
map.insert("server".into(), format!("{}:{}", host, port).into());
map.insert("bypass".into(), bypass.as_str().into());
Ok(map)
Ok(SystemProxy {
enable: *enable,
server: format!("{host}:{port}"),
bypass: bypass.to_string(),
})
}
#[tauri::command]
pub async fn get_auto_proxy() -> CmdResult<Mapping> {
#[specta::specta]
pub async fn get_auto_proxy() -> CmdResult<AutoProxy> {
Sysopt::global().wait_idle().await;
let auto_proxy = match Autoproxy::get_auto_proxy() {
Err(error) if proxy_control::is_missing_network_service(&error) => Autoproxy::default(),
@@ -39,18 +39,20 @@ pub async fn get_auto_proxy() -> CmdResult<Mapping> {
};
let Autoproxy { ref enable, ref url } = auto_proxy;
let mut map = Mapping::new();
map.insert("enable".into(), (*enable).into());
map.insert("url".into(), url.as_str().into());
Ok(map)
Ok(AutoProxy {
enable: *enable,
url: url.to_string(),
})
}
#[tauri::command]
#[specta::specta]
pub fn get_embedded_server_port() -> CmdResult<u16> {
crate::utils::server::embedded_server_port().stringify_err()
}
#[tauri::command]
#[specta::specta]
pub fn get_system_hostname() -> String {
match gethostname().into_string() {
Ok(name) => name,
@@ -62,12 +64,14 @@ pub fn get_system_hostname() -> String {
}
#[tauri::command]
#[specta::specta]
pub fn get_network_interfaces() -> Vec<String> {
tauri_plugin_clash_verge_sysinfo::list_network_interfaces()
}
#[tauri::command]
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
#[specta::specta]
pub fn get_network_interfaces_info() -> CmdResult<Vec<crate::ipc::NetworkInterfaceView>> {
use network_interface::{NetworkInterface, NetworkInterfaceConfig as _};
let names = get_network_interfaces();
@@ -77,9 +81,22 @@ pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
for interface in interfaces {
if names.contains(&interface.name) {
result.push(interface);
result.push(interface.into());
}
}
Ok(result)
}
#[derive(serde::Serialize, specta::Type)]
pub struct SystemProxy {
pub enable: bool,
pub server: String,
pub bypass: String,
}
#[derive(serde::Serialize, specta::Type)]
pub struct AutoProxy {
pub enable: bool,
pub url: String,
}
+31 -10
View File
@@ -33,6 +33,7 @@ fn profile_import_error(err: &anyhow::Error) -> std::string::String {
}
#[tauri::command]
#[specta::specta]
pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
let draft = Config::profiles().await;
let data = draft.data_arc();
@@ -40,6 +41,7 @@ pub async fn get_profiles() -> CmdResult<SharedDraft<IProfiles>> {
}
#[tauri::command]
#[specta::specta]
pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
match feat::enhance_profiles().await {
Ok(outcome) if outcome.is_valid() => {
@@ -65,6 +67,7 @@ pub async fn enhance_profiles() -> CmdResult<ValidationOutcome> {
#[tauri::command]
#[tracing::instrument(skip_all, level = "info", fields(url = %help::mask_url(&url), uid = tracing::field::Empty))]
#[specta::specta]
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
Ok(it) => it,
@@ -94,7 +97,10 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
}
#[tauri::command]
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
#[specta::specta]
pub async fn reorder_profile(active_id: std::string::String, over_id: std::string::String) -> CmdResult {
let active_id: String = active_id.into();
let over_id: String = over_id.into();
match profiles_reorder_safe(&active_id, &over_id).await {
Ok(_) => {
logging!(info, Type::Cmd, "重新排序配置文件: {} -> {}", active_id, over_id);
@@ -114,7 +120,9 @@ pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
}
#[tauri::command]
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
#[specta::specta]
pub async fn create_profile(item: PrfItem, file_data: Option<std::string::String>) -> CmdResult {
let file_data = file_data.map(String::from);
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
profiles_save_file_safe()
@@ -131,7 +139,9 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
}
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
#[specta::specta]
pub async fn update_profile(index: std::string::String, option: Option<PrfOption>) -> CmdResult {
let index: String = index.into();
match feat::update_profile(&index, option.as_ref(), true).await {
Ok(_) => Ok(()),
Err(e) => {
@@ -142,7 +152,9 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
}
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
#[specta::specta]
pub async fn delete_profile(index: std::string::String) -> CmdResult {
let index: String = index.into();
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = Config::profiles().await;
@@ -315,6 +327,7 @@ async fn perform_config_update(
#[tauri::command]
#[tracing::instrument(skip_all, level = "info", fields(target = ?profiles.current))]
#[specta::specta]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationOutcome> {
if CURRENT_SWITCHING_PROFILE
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
@@ -345,7 +358,9 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm
}
#[tauri::command]
pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
#[specta::specta]
pub async fn patch_profile(index: std::string::String, profile: PrfItem) -> CmdResult {
let index: String = index.into();
let profiles = Config::profiles().await;
let should_refresh_timer = if let Ok(old_profile) = profiles.latest_arc().get_item(&index)
&& let Some(new_option) = profile.option.as_ref()
@@ -385,7 +400,9 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
}
#[tauri::command]
pub async fn view_profile(index: String) -> CmdResult {
#[specta::specta]
pub async fn view_profile(index: std::string::String) -> CmdResult {
let index: String = index.into();
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
let file = profiles_ref
@@ -409,7 +426,9 @@ pub async fn view_profile(index: String) -> CmdResult {
}
#[tauri::command]
pub async fn read_profile_file(index: String) -> CmdResult<String> {
#[specta::specta]
pub async fn read_profile_file(index: std::string::String) -> CmdResult<std::string::String> {
let index: String = index.into();
let item = {
let profiles = Config::profiles().await;
let profiles_ref = profiles.latest_arc();
@@ -429,7 +448,7 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
.join(file.as_str());
match tokio::fs::try_exists(&path).await {
Ok(true) => {}
Ok(false) => return Ok(String::new()),
Ok(false) => return Ok(std::string::String::new()),
Err(err) => {
return Err(coded_error(
"PROFILE_READ_FAILED",
@@ -440,11 +459,13 @@ pub async fn read_profile_file(index: String) -> CmdResult<String> {
}
let data = item.read_file().await.with_error_code("PROFILE_READ_FAILED")?;
Ok(data)
Ok(data.into())
}
#[tauri::command]
pub async fn get_next_update_time(uid: String) -> CmdResult<Option<i64>> {
#[specta::specta]
pub async fn get_next_update_time(uid: std::string::String) -> CmdResult<Option<i64>> {
let uid: String = uid.into();
let timer = Timer::global();
let next_time = timer.get_next_update_time(&uid).await;
Ok(next_time)
+4
View File
@@ -18,6 +18,7 @@ use std::{
/// Merges one selection pair into fresh backend state to avoid stale-list overwrites.
#[tauri::command]
#[specta::specta]
pub async fn record_selected_node(group_name: String, node: String) -> CmdResult<()> {
crate::config::profiles::record_selected_node(&group_name, &node)
.await
@@ -25,6 +26,7 @@ pub async fn record_selected_node(group_name: String, node: String) -> CmdResult
}
#[tauri::command]
#[specta::specta]
pub async fn forget_selected_node(group_name: String) -> CmdResult<()> {
crate::config::profiles::forget_selected_node(&group_name)
.await
@@ -104,6 +106,7 @@ fn runtime_group_scopes(config: Option<&Mapping>) -> BTreeMap<String, GroupScope
}
#[tauri::command]
#[specta::specta]
pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
let runtime = Config::runtime().await;
let latest_runtime = runtime.latest_arc();
@@ -123,6 +126,7 @@ pub async fn get_proxy_view() -> CmdResult<ProxyViewV1> {
}
#[tauri::command]
#[specta::specta]
pub async fn sync_tray_proxy_selection() -> CmdResult<()> {
if TRAY_SYNC_RUNNING
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
+35 -19
View File
@@ -2,43 +2,58 @@ use super::CmdResult;
use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter};
use anyhow::{Context as _, anyhow};
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::collections::HashMap;
#[tauri::command]
pub async fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().await.latest_arc().config.clone())
#[specta::specta]
pub async fn get_runtime_config() -> CmdResult<Option<crate::ipc::YamlMapping>> {
Ok(Config::runtime()
.await
.latest_arc()
.config
.clone()
.map(crate::ipc::YamlMapping))
}
#[tauri::command]
pub async fn get_runtime_yaml() -> CmdResult<String> {
#[specta::specta]
pub async fn get_runtime_yaml() -> CmdResult<std::string::String> {
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
let config = runtime.config.as_ref();
config
.ok_or_else(|| anyhow!("failed to parse config to yaml file"))
.and_then(|config| {
yaml_emitter::to_mihomo_config_string(config)
.context("failed to convert config to yaml")
.map(|s| s.into())
})
.and_then(|config| yaml_emitter::to_mihomo_config_string(config).context("failed to convert config to yaml"))
.stringify_err()
}
#[tauri::command]
pub async fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().await.latest_arc().chain_logs.clone())
#[specta::specta]
pub async fn get_runtime_logs() -> CmdResult<RuntimeLogs> {
Ok(RuntimeLogs(Config::runtime().await.latest_arc().chain_logs.clone()))
}
#[derive(serde::Serialize, specta::Type)]
#[serde(transparent)]
pub struct RuntimeLogs(
#[specta(type = HashMap<std::string::String, Vec<(std::string::String, std::string::String)>>)]
HashMap<String, Vec<(String, String)>>,
);
#[tauri::command]
#[specta::specta]
pub fn take_discarded_keys_notice() -> Option<std::string::String> {
crate::enhance::take_discarded_keys_notice().map(Into::into)
}
#[tauri::command]
pub fn take_discarded_keys_notice() -> Option<String> {
crate::enhance::take_discarded_keys_notice()
}
#[tauri::command]
pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> CmdResult<String> {
#[specta::specta]
pub async fn get_runtime_proxy_chain_config(
proxy_chain_exit_node: std::string::String,
) -> CmdResult<std::string::String> {
let proxy_chain_exit_node: String = proxy_chain_exit_node.into();
let runtime = Config::runtime().await;
let runtime = runtime.latest_arc();
@@ -79,7 +94,6 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
yaml_emitter::to_mihomo_config_string(&config)
.context("YAML generation failed")
.map(|s| s.into())
.stringify_err()
} else {
Err("failed to get proxies or proxy-groups".into())
@@ -87,7 +101,9 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm
}
#[tauri::command]
pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<serde_yaml_ng::Value>) -> CmdResult<()> {
#[specta::specta]
pub async fn update_proxy_chain_config_in_runtime(proxy_chain_config: Option<crate::ipc::YamlValue>) -> CmdResult<()> {
let proxy_chain_config = proxy_chain_config.map(|value| value.0);
match CoreManager::global()
.update_runtime_config(|d| d.update_proxy_chain_config(proxy_chain_config))
.await
+7 -1
View File
@@ -16,7 +16,13 @@ use tokio::fs;
/// 保存profiles的配置
#[tauri::command]
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult<ValidationOutcome> {
#[specta::specta]
pub async fn save_profile_file(
index: std::string::String,
file_data: Option<std::string::String>,
) -> CmdResult<ValidationOutcome> {
let index: String = index.into();
let file_data = file_data.map(String::from);
let file_data = match file_data {
Some(d) => d,
None => return Ok(ValidationOutcome::Valid),
+13 -2
View File
@@ -8,11 +8,11 @@ use crate::{
},
};
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ServiceInstallOutcome {
Installed,
#[cfg(any(windows, test))]
#[cfg_attr(not(any(windows, test)), allow(dead_code))]
Sidecar {
reason: String,
},
@@ -82,11 +82,13 @@ where
}
#[tauri::command]
#[specta::specta]
pub async fn install_service() -> CmdResult<ServiceInstallOutcome> {
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
}
#[tauri::command]
#[specta::specta]
pub async fn uninstall_service() -> CmdResult {
CoreManager::global()
.uninstall_service_and_start_sidecar()
@@ -95,16 +97,19 @@ pub async fn uninstall_service() -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub async fn reinstall_service() -> CmdResult<ServiceInstallOutcome> {
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
}
#[tauri::command]
#[specta::specta]
pub async fn repair_service() -> CmdResult<ServiceInstallOutcome> {
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
}
#[tauri::command]
#[specta::specta]
pub async fn continue_with_sidecar() -> CmdResult {
crate::core::CoreManager::global()
.continue_with_sidecar(None)
@@ -113,31 +118,37 @@ pub async fn continue_with_sidecar() -> CmdResult {
}
#[tauri::command]
#[specta::specta]
pub fn get_sidecar_failure() -> crate::core::notification::SidecarFailureSnapshot {
crate::core::notification::sidecar_failure_snapshot()
}
#[tauri::command]
#[specta::specta]
pub fn take_service_fallback_notice() -> Option<crate::core::service::ServiceFallbackNotice> {
crate::core::service::take_service_fallback_notice()
}
#[tauri::command]
#[specta::specta]
pub fn get_core_startup_error() -> Option<crate::core::manager::CoreFailure> {
crate::core::CoreManager::global().get_startup_error()
}
#[tauri::command]
#[specta::specta]
pub fn take_service_repair_notice() -> bool {
crate::core::service::take_service_repair_notice()
}
#[tauri::command]
#[specta::specta]
pub fn take_service_owner_notice() -> Option<String> {
crate::core::service::take_service_owner_notice()
}
#[tauri::command]
#[specta::specta]
pub fn sync_runtime_providers() {
if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) {
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
+2
View File
@@ -5,11 +5,13 @@ use crate::core::{
/// Returns one coherent core/service snapshot instead of independently refreshed state.
#[tauri::command]
#[specta::specta]
pub async fn get_runtime_state() -> Result<RunStateView, String> {
Ok(RUN_STATE.settled().await.to_view())
}
#[tauri::command]
#[specta::specta]
pub async fn get_pending_failures() -> Vec<PendingFailure> {
notification::pending_failures()
}
+1
View File
@@ -22,6 +22,7 @@ mod platform {
}
#[tauri::command]
#[specta::specta]
pub async fn invoke_uwp_tool() -> CmdResult {
platform::invoke_uwp_tool()
}
+2
View File
@@ -9,12 +9,14 @@ use clash_verge_draft::SharedDraft;
/// 获取Verge配置
#[tauri::command]
#[specta::specta]
pub async fn get_verge_config() -> CmdResult<SharedDraft<IVerge>> {
feat::fetch_verge_config().await.stringify_err()
}
/// 修改Verge配置
#[tauri::command]
#[specta::specta]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
let operation = system_proxy_operation(&payload);
let result = match operation {
+22 -6
View File
@@ -4,11 +4,18 @@ use crate::{
config::{Config, IVerge},
core, feat,
};
use reqwest_dav::list_cmd::ListFile;
use smartstring::alias::String;
#[tauri::command]
pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> {
#[specta::specta]
pub async fn save_webdav_config(
url: std::string::String,
username: std::string::String,
password: std::string::String,
) -> CmdResult<()> {
let url: String = url.into();
let username: String = username.into();
let password: String = password.into();
let patch = IVerge {
webdav_url: Some(url),
webdav_username: Some(username),
@@ -25,22 +32,31 @@ pub async fn save_webdav_config(url: String, username: String, password: String)
}
#[tauri::command]
#[specta::specta]
pub async fn create_webdav_backup() -> CmdResult<()> {
feat::create_backup_and_upload_webdav().await.stringify_err()
}
#[tauri::command]
pub async fn list_webdav_backup() -> CmdResult<Vec<ListFile>> {
feat::list_wevdav_backup().await.stringify_err()
#[specta::specta]
pub async fn list_webdav_backup() -> CmdResult<Vec<crate::ipc::WebDavFile>> {
feat::list_wevdav_backup()
.await
.map(|files| files.into_iter().map(Into::into).collect())
.stringify_err()
}
#[tauri::command]
pub async fn delete_webdav_backup(filename: String) -> CmdResult<()> {
#[specta::specta]
pub async fn delete_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::delete_webdav_backup(filename).await.stringify_err()
}
#[tauri::command]
pub async fn restore_webdav_backup(filename: String) -> CmdResult<()> {
#[specta::specta]
pub async fn restore_webdav_backup(filename: std::string::String) -> CmdResult<()> {
let filename: String = filename.into();
feat::restore_webdav_backup(filename)
.await
.map_err(|error| super::proxy_aware_error(&error))
+6 -6
View File
@@ -308,7 +308,7 @@ impl IClashTemp {
}
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
pub struct ClashInfo {
/// clash core port
pub mixed_port: u16,
@@ -364,14 +364,14 @@ fn test_clash_info() {
assert_eq!(get_case(8888, "192.168.1.1:80800"), get_result(8888, "127.0.0.1:9097"));
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(rename_all = "kebab-case")]
pub struct IClashExternalControllerCors {
pub allow_origins: Option<Vec<String>>,
pub allow_private_network: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(rename_all = "kebab-case")]
pub struct IClash {
pub mixed_port: Option<u16>,
@@ -387,7 +387,7 @@ pub struct IClash {
pub external_controller_cors: Option<IClashExternalControllerCors>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(rename_all = "kebab-case")]
pub struct IClashTUN {
pub enable: Option<bool>,
@@ -397,7 +397,7 @@ pub struct IClashTUN {
pub dns_hijack: Option<Vec<String>>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(rename_all = "kebab-case")]
pub struct IClashDNS {
pub enable: Option<bool>,
@@ -414,7 +414,7 @@ pub struct IClashDNS {
pub nameserver_policy: Option<Vec<String>>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(rename_all = "kebab-case")]
pub struct IClashFallbackFilter {
pub geoip: Option<bool>,
+2 -1
View File
@@ -106,10 +106,11 @@ impl DnsOverrideState {
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, Serialize, specta::Type)]
pub struct ProfileDnsSettings {
pub enabled: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub confirmation: Option<String>,
}
+23 -11
View File
@@ -27,22 +27,27 @@ pub(super) fn normalize_profile_home_url(raw: &str) -> Option<String> {
Some(url.to_string().into())
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[derive(Debug, Clone, Deserialize, Serialize, Default, specta::Type)]
#[serde(default)]
pub struct PrfItem {
#[specta(type = Option<std::string::String>)]
pub uid: Option<String>,
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
#[specta(type = Option<std::string::String>)]
pub itype: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub desc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -57,19 +62,24 @@ pub struct PrfItem {
pub option: Option<PrfOption>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub home: Option<String>,
#[serde(skip)]
#[specta(type = Option<std::string::String>)]
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(default)]
pub struct PrfSelected {
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, specta::Type)]
pub struct PrfExtra {
pub upload: u64,
pub download: u64,
@@ -77,9 +87,11 @@ pub struct PrfExtra {
pub expire: u64,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq, specta::Type)]
#[serde(default)]
pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -102,15 +114,15 @@ pub struct PrfOption {
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_auto_update: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub merge: Option<String>,
#[specta(type = Option<std::string::String>)]
pub script: Option<String>,
#[specta(type = Option<std::string::String>)]
pub rules: Option<String>,
#[specta(type = Option<std::string::String>)]
pub proxies: Option<String>,
#[specta(type = Option<std::string::String>)]
pub groups: Option<String>,
}
+3 -1
View File
@@ -48,8 +48,10 @@ const SELECTED_NODES_SETTLE_INTERVAL: Duration = Duration::from_secs(1);
const SELECTED_NODES_FIRST_PASS_BUDGET: Duration = Duration::from_secs(3);
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IProfiles {
#[specta(type = Option<std::string::String>)]
pub current: Option<String>,
pub items: Option<Vec<PrfItem>>,
+45 -14
View File
@@ -11,26 +11,30 @@ use serde::{Deserialize, Serialize};
use smartstring::alias::String;
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IVerge {
/// silent | error | warn | info | debug | trace
#[specta(type = Option<std::string::String>)]
pub app_log_level: Option<String>,
/// Maximum app log size in KiB.
pub app_log_max_size: Option<u64>,
pub app_log_max_count: Option<usize>,
#[specta(type = Option<std::string::String>)]
pub language: Option<String>,
/// `light` or `dark` or `system`
#[specta(type = Option<std::string::String>)]
pub theme_mode: Option<String>,
#[specta(type = Option<std::string::String>)]
pub tray_event: Option<String>,
#[specta(type = Option<std::string::String>)]
pub env_type: Option<String>,
#[specta(type = Option<std::string::String>)]
pub start_page: Option<String>,
#[specta(type = Option<std::string::String>)]
pub startup_script: Option<String>,
pub traffic_graph: Option<bool>,
@@ -48,18 +52,23 @@ pub struct IVerge {
#[cfg(target_os = "macos")]
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub tray_icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub menu_icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<Vec<std::string::String>>)]
pub menu_order: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub proxy_group_tools_position: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub notice_position: Option<String>,
pub collapse_navbar: Option<bool>,
@@ -84,39 +93,43 @@ pub struct IVerge {
pub enable_dns_settings: Option<bool>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
#[specta(type = std::collections::BTreeMap<std::string::String, super::dns::ProfileDnsSettings>)]
pub profile_dns_settings: std::collections::BTreeMap<String, super::dns::ProfileDnsSettings>,
pub use_default_bypass: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
pub proxy_auto_config: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub pac_file_content: Option<String>,
#[specta(type = Option<std::string::String>)]
pub proxy_host: Option<String>,
pub theme_setting: Option<IVergeTheme>,
#[specta(type = Option<Vec<std::string::String>>)]
pub web_ui_list: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub clash_core: Option<String>,
/// format: {func},{key}
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<Vec<std::string::String>>)]
pub hotkeys: Option<Vec<String>>,
pub enable_global_hotkey: Option<bool>,
#[specta(type = Option<crate::ipc::JsonValue>)]
pub home_cards: Option<serde_json::Value>,
pub auto_close_connection: Option<bool>,
pub auto_check_update: Option<bool>,
#[specta(type = Option<std::string::String>)]
pub default_latency_test: Option<String>,
pub default_latency_timeout: Option<i16>,
@@ -171,6 +184,7 @@ pub struct IVerge {
skip_serializing_if = "Option::is_none",
default
)]
#[specta(type = Option<std::string::String>)]
pub webdav_url: Option<String>,
#[serde(
@@ -179,6 +193,7 @@ pub struct IVerge {
skip_serializing_if = "Option::is_none",
default
)]
#[specta(type = Option<std::string::String>)]
pub webdav_username: Option<String>,
#[serde(
@@ -187,12 +202,14 @@ pub struct IVerge {
skip_serializing_if = "Option::is_none",
default
)]
#[specta(type = Option<std::string::String>)]
pub webdav_password: Option<String>,
#[cfg(target_os = "macos")]
pub enable_tray_speed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[specta(type = Option<std::string::String>)]
pub tray_proxy_groups_display_mode: Option<String>,
pub tray_inline_outbound_modes: Option<bool>,
@@ -209,27 +226,41 @@ pub struct IVerge {
pub enable_external_controller: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IVergeTestItem {
#[specta(type = Option<std::string::String>)]
pub uid: Option<String>,
#[specta(type = Option<std::string::String>)]
pub name: Option<String>,
#[specta(type = Option<std::string::String>)]
pub icon: Option<String>,
#[specta(type = Option<std::string::String>)]
pub url: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
#[derive(Default, Debug, Clone, Deserialize, Serialize, specta::Type)]
#[serde(default)]
pub struct IVergeTheme {
#[specta(type = Option<std::string::String>)]
pub primary_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub secondary_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub primary_text: Option<String>,
#[specta(type = Option<std::string::String>)]
pub secondary_text: Option<String>,
#[specta(type = Option<std::string::String>)]
pub info_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub error_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub warning_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub success_color: Option<String>,
#[specta(type = Option<std::string::String>)]
pub font_family: Option<String>,
#[specta(type = Option<std::string::String>)]
pub css_injection: Option<String>,
}
+6 -6
View File
@@ -38,7 +38,7 @@ pub(crate) fn proxy_listener_keys() -> impl Iterator<Item = &'static str> {
PROXY_LISTENERS.iter().map(|(key, _, _)| *key)
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct ProxyPortSettings {
pub mixed_port: u16,
@@ -48,27 +48,27 @@ pub struct ProxyPortSettings {
pub tproxy: ToggleableProxyPort,
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[derive(Clone, Copy, Debug, Deserialize, specta::Type)]
pub struct ToggleableProxyPort {
pub enabled: bool,
pub port: u16,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct ListenerProbe {
pub address: String,
pub transports: Vec<ListenerTransport>,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize, specta::Type)]
#[serde(rename_all = "lowercase")]
pub enum ListenerTransport {
Tcp,
Udp,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ListenerProbeOutcome {
Available,
@@ -77,7 +77,7 @@ pub enum ListenerProbeOutcome {
Indeterminate { message: String },
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum SaveProxyPortsOutcome {
Saved,
+2 -2
View File
@@ -57,7 +57,7 @@ fn claim_core_readiness_generation(state: &AtomicU64, captured_generation: u64)
.is_ok()
}
#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, serde::Serialize, PartialEq, Eq, specta::Type)]
pub enum RunningMode {
Service,
Sidecar,
@@ -75,7 +75,7 @@ impl fmt::Display for RunningMode {
}
/// Why the Core is not running, kept until a Core starts again.
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq)]
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, specta::Type)]
#[serde(tag = "kind", content = "detail", rename_all = "camelCase")]
pub enum CoreFailure {
StartFailed(String),
+1 -4
View File
@@ -52,10 +52,7 @@ impl Handle {
/// Sent on every transition, so the frontend does not have to poll to notice that the Core
/// stopped or that the Service came back.
pub fn notify_run_state(state: &crate::core::runstate::RunStateView) {
let Ok(state) = serde_json::to_value(state) else {
return;
};
Self::send_event(FrontendEvent::RunStateChanged { state });
Self::send_event(FrontendEvent::RunStateChanged { state: state.clone() });
}
pub fn notify_profile_changed(profile_id: &String) {
+2 -2
View File
@@ -3,9 +3,8 @@ pub use announce::{Refresh, announce};
pub mod desktop;
pub mod handle;
pub mod notification;
pub use notification::frontend_wire_contract;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
pub enum NoticeStatus {
#[serde(rename = "info")]
Info,
@@ -83,6 +82,7 @@ impl NoticeStatus {
/// Every variant in golden order. The wire-contract export and the golden
/// test both derive their variant list from here; expected strings stay
/// hand-written in the test so renames cannot slip through.
#[cfg(test)]
pub const ALL: [Self; 34] = [
Self::Info,
Self::ImportSubUrlOk,
+58 -47
View File
@@ -1,6 +1,7 @@
use super::NoticeStatus;
use clash_verge_logging::{Type, logging};
use parking_lot::Mutex;
#[cfg(test)]
use serde_json::json;
use smartstring::alias::String;
use std::sync::Arc;
@@ -31,7 +32,7 @@ pub enum FrontendEvent<'a> {
uid: &'a String,
},
RunStateChanged {
state: serde_json::Value,
state: crate::core::runstate::RunStateView,
},
PendingFailuresChanged,
#[cfg(target_os = "linux")]
@@ -42,7 +43,7 @@ pub enum FrontendEvent<'a> {
/// Operation associated with a pending failure.
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum FailedOperation {
SystemProxyEnable,
@@ -72,12 +73,14 @@ impl FailedOperation {
}
/// Latest unresolved failure for one stable code.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct PendingFailure {
/// Stable code used as the table key.
#[specta(type = std::string::String)]
pub code: String,
/// Full diagnostic context chain.
#[specta(type = std::string::String)]
pub detail: String,
pub operation: FailedOperation,
/// Monotonic identity for repeated failures under the same code.
@@ -241,9 +244,10 @@ pub fn what_was_asked() -> FailedOperation {
static PENDING_FAILURES: LazyLock<FailureTable> = LazyLock::new(FailureTable::default);
/// A removal has a revision too, so a delayed read cannot restore a resolved failure.
#[derive(Debug, Default, Clone, serde::Serialize)]
#[derive(Debug, Default, Clone, serde::Serialize, specta::Type)]
pub struct SidecarFailureSnapshot {
pub revision: u64,
#[specta(type = Option<std::string::String>)]
pub detail: Option<String>,
}
@@ -327,24 +331,7 @@ impl NotificationSystem {
}
fn serialize_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
match event {
FrontendEvent::RefreshClash => ("verge://refresh-clash-config", Ok(json!("yes"))),
FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))),
FrontendEvent::RefreshProfiles => ("verge://refresh-profiles", Ok(json!("yes"))),
FrontendEvent::RefreshProxyConfig => ("verge://refresh-proxy-config", Ok(serde_json::Value::Null)),
FrontendEvent::NoticeMessage { status, message } => (
"verge://notice-message",
serde_json::to_value((status, message.as_ref())),
),
FrontendEvent::ProfileChanged { current_profile_id } => ("profile-changed", Ok(json!(current_profile_id))),
FrontendEvent::TimerUpdated { profile_index } => ("verge://timer-updated", Ok(json!(profile_index))),
FrontendEvent::ProfileUpdateStarted { uid } => ("profile-update-started", Ok(json!({ "uid": uid }))),
FrontendEvent::ProfileUpdateCompleted { uid } => ("profile-update-completed", Ok(json!({ "uid": uid }))),
FrontendEvent::RunStateChanged { state } => ("verge://run-state-changed", Ok(state)),
FrontendEvent::PendingFailuresChanged => ("verge://pending-failures-changed", Ok(serde_json::Value::Null)),
#[cfg(target_os = "linux")]
FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)),
}
serialize_frontend_event(event)
}
pub(crate) fn send_event(app_handle: AppHandle, event: FrontendEvent) {
@@ -381,8 +368,17 @@ mod wire_tests {
}
/// One sample per `FrontendEvent` variant, paired with its wire name and payload.
/// Shared by the wire golden test and the contract export so the two cannot drift.
#[cfg(test)]
fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str, serde_json::Value)> {
let state = crate::core::runstate::RunState {
health: Default::default(),
pending: None,
sidecar_allowed: false,
mode: crate::core::manager::RunningMode::NotRunning,
is_admin: false,
op_in_flight: false,
}
.to_view();
vec![
(
FrontendEvent::RefreshClash,
@@ -414,11 +410,9 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
json!("profile"),
),
(
FrontendEvent::RunStateChanged {
state: json!({"running": true}),
},
FrontendEvent::RunStateChanged { state: state.clone() },
"verge://run-state-changed",
json!({"running": true}),
serde_json::to_value(state).unwrap_or(serde_json::Value::Null),
),
(
FrontendEvent::PendingFailuresChanged,
@@ -445,24 +439,41 @@ fn sample_frontend_events(uid: &String) -> Vec<(FrontendEvent<'_>, &'static str,
]
}
/// Machine-readable form of the frontend wire contract: every event name with a
/// serialized sample payload, plus every notice status string. The sample
/// payload's JSON shape (string / null / array / object) is what the frontend
/// generator maps to TypeScript types. `ThemeChanged` is a Tauri built-in
/// linux-only window event, not part of this contract.
pub fn frontend_wire_contract() -> serde_json::Value {
let uid = String::from("sample");
let events: Vec<_> = sample_frontend_events(&uid)
.into_iter()
.map(|(event, _, _)| {
let (name, payload) = NotificationSystem::serialize_event(event);
json!({ "name": name, "sample": payload.unwrap_or(serde_json::Value::Null) })
})
.collect();
let statuses: Vec<_> = NoticeStatus::ALL
.iter()
// A unit variant with a string rename cannot fail to serialize.
.filter_map(|status| serde_json::to_value(status).ok())
.collect();
json!({ "version": 1, "events": events, "noticeStatuses": statuses })
macro_rules! frontend_events {
($( $field:ident: $payload:ty = $name:literal, $pattern:pat => $value:expr; )*) => {
#[derive(serde::Serialize, specta::Type)]
pub struct VergeEventPayloads {
$(#[serde(rename = $name)] pub $field: $payload,)*
}
fn serialize_frontend_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
match event {
$($pattern => {
let payload: $payload = $value;
($name, serde_json::to_value(payload))
},)*
#[cfg(target_os = "linux")]
FrontendEvent::ThemeChanged { theme } => ("tauri://theme-changed", serde_json::to_value(theme)),
}
}
};
}
frontend_events! {
refresh_clash: std::string::String = "verge://refresh-clash-config", FrontendEvent::RefreshClash => "yes".to_owned();
refresh_verge: std::string::String = "verge://refresh-verge-config", FrontendEvent::RefreshVerge => "yes".to_owned();
refresh_profiles: std::string::String = "verge://refresh-profiles", FrontendEvent::RefreshProfiles => "yes".to_owned();
refresh_proxy: () = "verge://refresh-proxy-config", FrontendEvent::RefreshProxyConfig => ();
notice: (NoticeStatus, std::string::String) = "verge://notice-message", FrontendEvent::NoticeMessage { status, ref message } => (status, message.to_string());
profile_changed: std::string::String = "profile-changed", FrontendEvent::ProfileChanged { current_profile_id } => current_profile_id.to_string();
timer_updated: std::string::String = "verge://timer-updated", FrontendEvent::TimerUpdated { profile_index } => profile_index.to_string();
profile_started: ProfileUpdatePayload = "profile-update-started", FrontendEvent::ProfileUpdateStarted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
profile_completed: ProfileUpdatePayload = "profile-update-completed", FrontendEvent::ProfileUpdateCompleted { uid } => ProfileUpdatePayload { uid: uid.to_string() };
run_state: crate::core::runstate::RunStateView = "verge://run-state-changed", FrontendEvent::RunStateChanged { state } => state;
pending_failures: () = "verge://pending-failures-changed", FrontendEvent::PendingFailuresChanged => ();
}
#[derive(serde::Serialize, specta::Type)]
pub struct ProfileUpdatePayload {
pub uid: std::string::String,
}
+16 -12
View File
@@ -20,7 +20,7 @@ pub struct GroupScope {
pub struct ProxyViewBuilder;
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct ProxyViewV1 {
pub schema_version: u8,
@@ -34,21 +34,21 @@ pub struct ProxyViewV1 {
pub providers: Vec<ProxyProviderView>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "lowercase")]
pub enum ProxyViewOrderSource {
Runtime,
Fallback,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "lowercase")]
pub enum ProxyViewProviderState {
Ready,
Unavailable,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
pub struct ProxyCapabilities {
pub udp: bool,
pub xudp: bool,
@@ -57,11 +57,12 @@ pub struct ProxyCapabilities {
pub smux: bool,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct ProxyGroupView {
pub name: String,
#[serde(rename = "type")]
#[specta(type = String)]
pub proxy_type: ProxyType,
pub alive: bool,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -74,20 +75,23 @@ pub struct ProxyGroupView {
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_url: Option<String>,
#[specta(type = Vec<crate::ipc::DelayHistory>)]
pub history: Vec<DelayHistory>,
#[serde(flatten)]
pub capabilities: ProxyCapabilities,
pub members: Vec<ProxyMemberRef>,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct ProxyNodeView {
pub record_id: String,
pub name: String,
#[serde(rename = "type")]
#[specta(type = String)]
pub proxy_type: ProxyType,
pub alive: bool,
#[specta(type = Vec<crate::ipc::DelayHistory>)]
pub history: Vec<DelayHistory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
@@ -102,7 +106,7 @@ pub struct ProxyNodeView {
pub source: ProxyNodeSource,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum ProxyNodeSource {
Core {
@@ -117,7 +121,7 @@ pub enum ProxyNodeSource {
},
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum ProxyMemberRef {
Group {
@@ -134,7 +138,7 @@ pub enum ProxyMemberRef {
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
pub enum ProxyMemberUnresolvedReason {
#[serde(rename = "missing")]
Missing,
@@ -144,7 +148,7 @@ pub enum ProxyMemberUnresolvedReason {
ProviderUnavailable,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct ProxyProviderView {
pub name: String,
@@ -156,7 +160,7 @@ pub struct ProxyProviderView {
pub proxy_record_ids: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, specta::Type)]
pub enum ProxyProviderVehicleType {
#[serde(rename = "HTTP")]
Http,
@@ -166,7 +170,7 @@ pub enum ProxyProviderVehicleType {
Inline,
}
#[derive(Debug, PartialEq, Eq, Serialize)]
#[derive(Debug, PartialEq, Eq, Serialize, specta::Type)]
pub struct ProxySubscriptionInfo {
pub upload: i64,
pub download: i64,
+21 -11
View File
@@ -14,13 +14,13 @@ pub enum ServiceHealth {
}
impl ServiceHealth {
const fn kind(&self) -> &'static str {
const fn kind(&self) -> ServiceHealthKind {
match self {
Self::Unknown => "unknown",
Self::Ready => "ready",
Self::NotInstalled => "notInstalled",
Self::VersionMismatch => "versionMismatch",
Self::Unavailable(_) => "unavailable",
Self::Unknown => ServiceHealthKind::Unknown,
Self::Ready => ServiceHealthKind::Ready,
Self::NotInstalled => ServiceHealthKind::NotInstalled,
Self::VersionMismatch => ServiceHealthKind::VersionMismatch,
Self::Unavailable(_) => ServiceHealthKind::Unavailable,
}
}
@@ -32,7 +32,7 @@ impl ServiceHealth {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum PendingAction {
Install,
@@ -118,11 +118,11 @@ impl RunState {
}
}
#[derive(Debug, Clone, serde::Serialize)]
#[derive(Debug, Clone, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct RunStateView {
pub mode: RunningMode,
pub service: &'static str,
pub service: ServiceHealthKind,
pub service_unavailable_reason: Option<String>,
pub pending_action: Option<PendingAction>,
pub sidecar_allowed: bool,
@@ -232,7 +232,7 @@ mod tests {
let view = run_state.to_view();
assert_eq!(view.service, "unavailable");
assert_eq!(view.service, ServiceHealthKind::Unavailable);
assert_eq!(view.service_unavailable_reason.as_deref(), Some("socket refused"));
assert_eq!(view.pending_action, Some(PendingAction::Reinstall));
assert!(view.tun_capable, "elevation alone makes TUN possible");
@@ -244,7 +244,7 @@ mod tests {
fn a_healthy_view_reports_no_unavailable_reason() {
let view = state(ServiceHealth::Ready, false, false).to_view();
assert_eq!(view.service, "ready");
assert_eq!(view.service, ServiceHealthKind::Ready);
assert_eq!(view.service_unavailable_reason, None);
assert!(view.service_usable);
assert!(!view.service_needs_attention);
@@ -350,3 +350,13 @@ mod tests {
assert!(settled.tun_should_be_disabled(true));
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum ServiceHealthKind {
Unknown,
Ready,
NotInstalled,
VersionMismatch,
Unavailable,
}
+1 -1
View File
@@ -44,7 +44,7 @@ static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false);
static PENDING_SERVICE_OWNER_NOTICE: Mutex<Option<String>> = Mutex::new(None);
/// Why the Service was unavailable when the core fell back to Sidecar.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)]
#[serde(tag = "kind", content = "reason", rename_all = "camelCase")]
pub enum ServiceFallbackNotice {
Unavailable,
+2 -2
View File
@@ -34,7 +34,7 @@ impl SilentUpdater {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, specta::Type)]
struct UpdateCacheMeta {
version: String,
downloaded_at: String,
@@ -214,7 +214,7 @@ fn nsis_language_id(app_language: &str) -> &'static str {
}
/// Same wire shape as the updater plugin's JS `DownloadEvent`.
#[derive(Clone, Serialize)]
#[derive(Clone, Serialize, specta::Type)]
#[serde(tag = "event", content = "data")]
pub enum DownloadEvent {
#[serde(rename_all = "camelCase")]
+11 -5
View File
@@ -51,7 +51,7 @@ pub struct CoreConfigValidator {
is_processing: AtomicBool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum ValidationErrorKind {
FileMissing,
@@ -94,7 +94,7 @@ impl ValidationErrorKind {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub enum ValidationSkipReason {
Exiting,
@@ -110,12 +110,18 @@ impl fmt::Display for ValidationSkipReason {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum ValidationOutcome {
Valid,
Invalid { kind: ValidationErrorKind, message: String },
Skipped { reason: ValidationSkipReason },
Invalid {
kind: ValidationErrorKind,
#[specta(type = std::string::String)]
message: String,
},
Skipped {
reason: ValidationSkipReason,
},
Busy,
}
+4 -1
View File
@@ -16,10 +16,13 @@ use smartstring::alias::String;
use std::path::PathBuf;
use tokio::fs;
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, specta::Type)]
pub struct LocalBackupFile {
#[specta(type = std::string::String)]
pub filename: String,
#[specta(type = std::string::String)]
pub path: String,
#[specta(type = std::string::String)]
pub last_modified: String,
pub content_length: u64,
}
+1 -1
View File
@@ -34,7 +34,7 @@ static STAGING_GENERATION: AtomicU64 = AtomicU64::new(0);
/// `.rollback` and `.old` are fixed paths, so two upgrades must not overlap.
static UPGRADE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
#[derive(Debug, serde::Serialize)]
#[derive(Debug, serde::Serialize, specta::Type)]
pub struct CoreUpgradeReport {
/// False when the managed core was already at the latest version.
pub upgraded: bool,
+5 -2
View File
@@ -15,11 +15,14 @@ use crate::{
utils::dirs,
};
#[derive(Serialize)]
#[derive(Serialize, specta::Type)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum DnsOverrideOutcome {
Applied,
ConfirmationRequired { source: String },
ConfirmationRequired {
#[specta(type = std::string::String)]
source: String,
},
}
pub async fn set_dns_override(
+4 -1
View File
@@ -8,10 +8,13 @@ use std::path::{Component, Path, PathBuf};
use tokio::fs;
use tokio::io::AsyncWriteExt as _;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[derive(Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct IconInfo {
#[specta(type = std::string::String)]
name: String,
#[specta(type = std::string::String)]
previous_t: String,
#[specta(type = std::string::String)]
current_t: String,
}
+236
View File
@@ -0,0 +1,236 @@
use crate::cmd;
pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
tauri_specta::Builder::new()
.types(&platform_types())
.commands(tauri_specta::collect_commands![
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info::<tauri::Wry>,
cmd::probe_listener,
cmd::save_proxy_ports,
cmd::get_sys_proxy,
cmd::get_auto_proxy,
cmd::get_embedded_server_port,
cmd::open_app_dir,
cmd::open_logs_dir,
cmd::open_core_dir,
cmd::get_network_interfaces,
cmd::get_system_hostname,
cmd::restart_app,
cmd::install_update,
cmd::cancel_update_download,
cmd::restart_core,
cmd::upgrade_clash_core,
cmd::get_runtime_state,
cmd::get_pending_failures,
cmd::get_sidecar_failure,
cmd::entry_lightweight_mode,
cmd::install_service,
cmd::uninstall_service,
cmd::reinstall_service,
cmd::repair_service,
cmd::continue_with_sidecar,
cmd::sync_runtime_providers,
cmd::get_clash_info,
cmd::patch_clash_config,
cmd::patch_clash_mode,
cmd::get_clash_mode,
cmd::change_clash_core,
cmd::get_runtime_config,
cmd::get_proxy_view,
cmd::get_runtime_yaml,
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node,
cmd::forget_selected_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::set_dns_override,
cmd::take_dns_override_notice,
cmd::take_service_fallback_notice,
cmd::get_core_startup_error,
cmd::take_service_repair_notice,
cmd::take_service_owner_notice,
cmd::take_discarded_keys_notice,
cmd::get_dns_config_content,
cmd::validate_dns_config,
cmd::get_clash_logs,
cmd::get_verge_config,
cmd::patch_verge_config,
cmd::test_delay,
cmd::get_app_dir,
cmd::copy_icon_file,
cmd::download_icon_cache,
cmd::open_devtools,
cmd::exit_app,
cmd::get_network_interfaces_info,
cmd::get_profiles,
cmd::enhance_profiles,
cmd::patch_profiles_config,
cmd::view_profile,
cmd::patch_profile,
cmd::create_profile,
cmd::import_profile,
cmd::reorder_profile,
cmd::update_profile,
cmd::delete_profile,
cmd::read_profile_file,
cmd::save_profile_file,
cmd::get_next_update_time,
cmd::create_local_backup,
cmd::list_local_backup,
cmd::delete_local_backup,
cmd::restore_local_backup,
cmd::import_local_backup,
cmd::export_local_backup,
cmd::create_webdav_backup,
cmd::save_webdav_config,
cmd::list_webdav_backup,
cmd::delete_webdav_backup,
cmd::restore_webdav_backup,
cmd::get_unlock_items,
cmd::check_media_unlock,
cmd::check_media_unlock_item,
])
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
.dangerously_cast_bigints_to_number()
.typ::<crate::core::notify::notification::VergeEventPayloads>()
}
// Tauri transports YAML values as JSON; keep YAML storage out of the IPC type graph.
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(transparent)]
pub struct YamlMapping(#[specta(type = std::collections::HashMap<String, JsonValue>)] pub serde_yaml_ng::Mapping);
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(transparent)]
pub struct YamlValue(#[specta(type = JsonValue)] pub serde_yaml_ng::Value);
#[derive(serde::Serialize, specta::Type)]
pub struct WebDavFile {
pub href: String,
#[specta(type = String)]
pub last_modified: chrono::DateTime<chrono::Utc>,
pub content_length: i64,
pub content_type: String,
pub tag: Option<String>,
}
impl From<reqwest_dav::list_cmd::ListFile> for WebDavFile {
fn from(file: reqwest_dav::list_cmd::ListFile) -> Self {
Self {
href: file.href,
last_modified: file.last_modified,
content_length: file.content_length,
content_type: file.content_type,
tag: file.tag,
}
}
}
#[derive(serde::Serialize, specta::Type)]
pub struct NetworkInterfaceView {
pub name: String,
#[specta(type = Vec<InterfaceAddress>)]
pub addr: Vec<network_interface::Addr>,
pub mac_addr: Option<String>,
pub index: u32,
pub internal: bool,
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
pub enum InterfaceAddress {
V4(IpAddress),
V6(IpAddress),
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
pub struct IpAddress {
pub ip: String,
pub broadcast: Option<String>,
pub netmask: Option<String>,
}
impl From<network_interface::NetworkInterface> for NetworkInterfaceView {
fn from(interface: network_interface::NetworkInterface) -> Self {
Self {
name: interface.name,
addr: interface.addr,
mac_addr: interface.mac_addr,
index: interface.index,
internal: interface.internal,
}
}
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
pub struct DelayHistory {
pub time: String,
pub delay: u16,
}
fn platform_types() -> specta::Types {
use specta::datatype::{DataType, Fields};
let mut types = specta::Types::default().register::<crate::config::IVerge>();
<PlatformVergeFields as specta::Type>::definition(&mut types);
let platform = types
.into_unsorted_iter()
.find(|ty| ty.name == "PlatformVergeFields")
.and_then(|ty| ty.ty.clone());
let Some(DataType::Struct(platform)) = platform else {
unreachable!()
};
let Fields::Named(platform) = platform.fields else {
unreachable!()
};
types.iter_mut(|ty| {
if ty.name != "IVerge" {
return;
}
let Some(DataType::Struct(config)) = &mut ty.ty else {
return;
};
let Fields::Named(fields) = &mut config.fields else {
return;
};
fields
.fields
.retain(|(name, _)| !platform.fields.iter().any(|(platform_name, _)| name == platform_name));
fields.fields.extend(platform.fields.iter().cloned());
});
types
}
// These fields exist only on some hosts; IPC generation covers every desktop target.
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
struct PlatformVergeFields {
#[specta(optional)]
tray_icon: Option<String>,
#[specta(optional)]
verge_redir_port: Option<u16>,
#[specta(optional)]
verge_redir_enabled: Option<bool>,
#[specta(optional)]
verge_tproxy_port: Option<u16>,
#[specta(optional)]
verge_tproxy_enabled: Option<bool>,
#[specta(optional)]
enable_tray_speed: Option<bool>,
}
#[derive(serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(untagged)]
pub enum JsonValue {
Null(()),
Bool(bool),
Number(f64),
String(String),
// Specta resolves fields in nested functions, where Self is unavailable.
Array(#[specta(type = Vec<JsonValue>)] Vec<Self>),
Object(#[specta(type = std::collections::HashMap<String, JsonValue>)] std::collections::HashMap<String, Self>),
}
+5 -97
View File
@@ -7,6 +7,7 @@ mod constants;
mod core;
mod enhance;
mod feat;
pub mod ipc;
mod process;
pub mod utils;
@@ -26,9 +27,6 @@ use tauri_plugin_deep_link::DeepLinkExt as _;
pub static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
// Re-exported for src/bin/frontend-contract.rs; the wire contract stays owned
// by core::notify.
pub use crate::core::notify::frontend_wire_contract;
/// Application initialization helper functions
mod app_init {
use super::*;
@@ -127,100 +125,10 @@ mod app_init {
}
pub fn generate_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Send + Sync + 'static {
tauri::generate_handler![
tauri_plugin_clash_verge_sysinfo::commands::get_system_info,
tauri_plugin_clash_verge_sysinfo::commands::get_app_uptime,
tauri_plugin_clash_verge_sysinfo::commands::export_diagnostic_info,
cmd::probe_listener,
cmd::save_proxy_ports,
cmd::get_sys_proxy,
cmd::get_auto_proxy,
cmd::get_embedded_server_port,
cmd::open_app_dir,
cmd::open_logs_dir,
cmd::open_core_dir,
cmd::get_network_interfaces,
cmd::get_system_hostname,
cmd::restart_app,
cmd::install_update,
cmd::cancel_update_download,
cmd::restart_core,
cmd::upgrade_clash_core,
cmd::get_runtime_state,
cmd::get_pending_failures,
cmd::get_sidecar_failure,
cmd::entry_lightweight_mode,
cmd::install_service,
cmd::uninstall_service,
cmd::reinstall_service,
cmd::repair_service,
cmd::continue_with_sidecar,
cmd::sync_runtime_providers,
cmd::get_clash_info,
cmd::patch_clash_config,
cmd::patch_clash_mode,
cmd::get_clash_mode,
cmd::change_clash_core,
cmd::get_runtime_config,
cmd::get_proxy_view,
cmd::get_runtime_yaml,
cmd::get_runtime_logs,
cmd::get_runtime_proxy_chain_config,
cmd::update_proxy_chain_config_in_runtime,
cmd::invoke_uwp_tool,
cmd::copy_clash_env,
cmd::sync_tray_proxy_selection,
cmd::record_selected_node,
cmd::forget_selected_node,
cmd::save_dns_config,
cmd::apply_dns_config,
cmd::set_dns_override,
cmd::take_dns_override_notice,
cmd::take_service_fallback_notice,
cmd::get_core_startup_error,
cmd::take_service_repair_notice,
cmd::take_service_owner_notice,
cmd::take_discarded_keys_notice,
cmd::get_dns_config_content,
cmd::validate_dns_config,
cmd::get_clash_logs,
cmd::get_verge_config,
cmd::patch_verge_config,
cmd::test_delay,
cmd::get_app_dir,
cmd::copy_icon_file,
cmd::download_icon_cache,
cmd::open_devtools,
cmd::exit_app,
cmd::get_network_interfaces_info,
cmd::get_profiles,
cmd::enhance_profiles,
cmd::patch_profiles_config,
cmd::view_profile,
cmd::patch_profile,
cmd::create_profile,
cmd::import_profile,
cmd::reorder_profile,
cmd::update_profile,
cmd::delete_profile,
cmd::read_profile_file,
cmd::save_profile_file,
cmd::get_next_update_time,
cmd::create_local_backup,
cmd::list_local_backup,
cmd::delete_local_backup,
cmd::restore_local_backup,
cmd::import_local_backup,
cmd::export_local_backup,
cmd::create_webdav_backup,
cmd::save_webdav_config,
cmd::list_webdav_backup,
cmd::delete_webdav_backup,
cmd::restore_webdav_backup,
cmd::get_unlock_items,
cmd::check_media_unlock,
cmd::check_media_unlock_item,
]
{
let builder = crate::ipc::builder();
move |invoke| builder.invoke_handler()(invoke)
}
}
}
+4 -4
View File
@@ -420,7 +420,7 @@ interface PersistentProxySelectProps
value: string
selectedName: string
groupName: string
fixed?: string
fixed?: string | null
selectedDelay: number
options: ProxyOption[]
onChange: (value: string) => void
@@ -768,8 +768,8 @@ export const CurrentProxyCard = () => {
// The profile selection is the durable source across core restarts and run modes.
handleSelectChange(
selectedGroup.name,
previousProxy,
selectedGroup.fixed,
previousProxy!,
selectedGroup.fixed!,
)({
target: { value: nextName },
})
@@ -917,7 +917,7 @@ export const CurrentProxyCard = () => {
unsortedProxyOptions,
sortType,
delays.of,
defaultLatencyTimeout,
defaultLatencyTimeout!,
),
[
delays,
+9 -25
View File
@@ -56,26 +56,7 @@ const openProfileHome = (url: string) => {
void openExternalUrl(url).catch(showNotice.error)
}
// 使用类型定义,而不是导入
interface ProfileExtra {
upload: number
download: number
total: number
expire: number
}
interface ProfileItem {
uid: string
type?: 'local' | 'remote' | 'merge' | 'script'
name?: string
desc?: string
file?: string
url?: string
updated?: number
extra?: ProfileExtra
home?: string
option?: any
}
type ProfileItem = IProfileItem
interface HomeProfileCardProps {
current: ProfileItem | null | undefined
@@ -298,10 +279,13 @@ export const HomeProfileCard = ({
setUpdating(true)
try {
await mutate(() => updateProfile(current.uid, current.option), {
id: `update-profile:${current.uid}`,
errorNotice: false,
})
await mutate(
() => updateProfile(current.uid!, current.option ?? undefined),
{
id: `update-profile:${current.uid}`,
errorNotice: false,
},
)
onProfileUpdated?.()
// 刷新首页数据
@@ -345,7 +329,7 @@ export const HomeProfileCard = ({
flex: 1,
},
}}
title={current.name}
title={current.name!}
>
<span>{current.name}</span>
<LaunchOutlined
+1 -1
View File
@@ -201,7 +201,7 @@ export const ProxyTunCard: FC = () => {
onClick={() => handleTabChange('tun')}
icon={TroubleshootRounded}
label={t('settings.sections.system.toggles.tunMode')}
hasIndicator={enable_tun_mode && isTunModeAvailable}
hasIndicator={enable_tun_mode! && isTunModeAvailable}
/>
</Stack>
+1 -1
View File
@@ -164,7 +164,7 @@ export const TestCard = () => {
}}
>
{testList.map((item, index) => (
<SortableItem key={item.uid} id={item.uid} index={index}>
<SortableItem key={item.uid} id={item.uid!} index={index}>
<TestItem
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
+18 -18
View File
@@ -120,7 +120,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
)
const setLoading = useCallback(
(loading: boolean) => {
setProfileLoading([itemData.uid], loading)
setProfileLoading([itemData.uid!], loading)
},
[itemData.uid, setProfileLoading],
)
@@ -140,7 +140,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
debugLog(`强制刷新定时器任务`)
}
const nextUpdate = await getNextUpdateTime(itemData.uid)
const nextUpdate = await getNextUpdateTime(itemData.uid!)
debugLog(`获取到下次更新时间结果:`, nextUpdate)
if (nextUpdate) {
@@ -245,7 +245,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const hasHome = !!itemData.home // only subscription url has home page
const { upload = 0, download = 0, total = 0 } = extra ?? {}
const from = parseUrl(itemData.url)
const from = parseUrl(itemData.url!)
const description = itemData.desc
const expire = parseExpire(extra?.expire)
const progress = Math.min(
@@ -253,7 +253,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
100,
)
const loading = loadingCache.has(itemData.uid)
const loading = loadingCache.has(itemData.uid!)
const [, forceRefresh] = useReducer((value: number) => value + 1, 0)
useEffect(() => {
@@ -263,7 +263,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const handler = () => {
const now = Date.now()
const lastUpdate = updated * 1000
const lastUpdate = updated! * 1000
if (now - lastUpdate >= 24 * 36e5) return
const wait = now - lastUpdate >= 36e5 ? 30e5 : 5e4
@@ -293,7 +293,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const [confirmOpen, setConfirmOpen] = useState(false)
const [qrOpen, setQrOpen] = useState(false)
const loadProfileDocument = useCallback(() => readProfileFile(uid), [uid])
const loadProfileDocument = useCallback(() => readProfileFile(uid!), [uid])
const loadMergeDocument = useCallback(
() => readProfileFile(option?.merge ?? ''),
[option?.merge],
@@ -370,7 +370,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const onOpenFile = useLockFn(async () => {
setAnchorEl(null)
try {
await mutate(() => viewProfile(itemData.uid), {
await mutate(() => viewProfile(itemData.uid!), {
id: 'view-profile',
errorNotice: false,
})
@@ -402,7 +402,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
try {
const payload = Object.keys(option).length > 0 ? option : undefined
await mutate(() => updateProfile(itemData.uid, payload), {
await mutate(() => updateProfile(itemData.uid!, payload), {
id: `update-profile:${itemData.uid}`,
errorNotice: false,
})
@@ -593,7 +593,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
const handleSaveProfileDocument = useLockFn(async () => {
const currentValue = profileDocument.value
const result = await mutate(() => saveProfileFile(uid, currentValue), {
const result = await mutate(() => saveProfileFile(uid!, currentValue), {
id: `save-profile-file:${uid}`,
errorNotice: false,
})
@@ -728,7 +728,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
variant="h6"
component="h2"
noWrap
title={name}
title={name!}
>
{name}
</Typography>
@@ -794,7 +794,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
title={
showNextUpdate
? t('profiles.components.profileItem.tooltips.showLast')
: `${t('shared.labels.updateTime')}: ${parseExpire(updated)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
: `${t('shared.labels.updateTime')}: ${parseExpire(updated!)}\n${t('profiles.components.profileItem.tooltips.showNext')}`
}
sx={{
fontSize: 14,
@@ -812,8 +812,8 @@ const ProfileItemBase = (props: ProfileItemProps) => {
>
{showNextUpdate
? nextUpdateTime
: updated > 0
? dayjs(updated * 1000).fromNow()
: updated! > 0
? dayjs(updated! * 1000).fromNow()
: ''}
</Typography>
</Box>
@@ -831,7 +831,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
) : (
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: 'flex-end' }}>
<span title={t('shared.labels.updateTime')}>
{parseExpire(updated)}
{parseExpire(updated!)}
</span>
</Box>
)}
@@ -896,7 +896,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
<RulesEditorViewer
groupsUid={option?.groups ?? ''}
mergeUid={option?.merge ?? ''}
profileUid={uid}
profileUid={uid!}
property={option?.rules ?? ''}
open={true}
onSave={onSave}
@@ -905,7 +905,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
)}
{proxiesOpen && (
<ProxiesEditorViewer
profileUid={uid}
profileUid={uid!}
property={option?.proxies ?? ''}
open={true}
onSave={onSave}
@@ -916,7 +916,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
<GroupsEditorViewer
mergeUid={option?.merge ?? ''}
proxiesUid={option?.proxies ?? ''}
profileUid={uid}
profileUid={uid!}
property={option?.groups ?? ''}
open={true}
onSave={onSave}
@@ -976,7 +976,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
{qrOpen && itemData.url && (
<QrViewer
open={true}
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name)}`}
value={`${itemData.url}${itemData.url.includes('?') ? '&' : '?'}name=${encodeURIComponent(name!)}`}
onClose={() => setQrOpen(false)}
/>
)}
+7 -7
View File
@@ -139,7 +139,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
t('profiles.modals.profileForm.errors.uidMissing'),
)
}
await mutate(() => patchProfile(form.uid, item), {
await mutate(() => patchProfile(form.uid!, item), {
id: 'patch-profile',
errorNotice: false,
})
@@ -157,7 +157,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
t('profiles.modals.profileForm.errors.uidMissing'),
)
}
await mutate(() => patchProfile(form.uid, item), {
await mutate(() => patchProfile(form.uid!, item), {
id: 'patch-profile',
errorNotice: false,
})
@@ -190,13 +190,13 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
t('profiles.modals.profileForm.errors.uidMissing'),
)
}
await mutate(() => patchProfile(form.uid, retryItem), {
await mutate(() => patchProfile(form.uid!, retryItem), {
id: 'patch-profile',
errorNotice: false,
})
await mutate(
() => patchProfile(form.uid, { option: originalOptions }),
() => patchProfile(form.uid!, { option: originalOptions }),
{
id: 'patch-profile',
errorNotice: false,
@@ -413,7 +413,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.useSystemProxy')}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
<Switch checked={field.value!} {...field} color="primary" />
</StyledBox>
)}
/>
@@ -426,7 +426,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.useClashProxy')}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
<Switch checked={field.value!} {...field} color="primary" />
</StyledBox>
)}
/>
@@ -439,7 +439,7 @@ export function ProfileViewer({ onChange, ref }: ProfileViewerProps) {
<InputLabel>
{t('profiles.modals.profileForm.fields.acceptInvalidCerts')}
</InputLabel>
<Switch checked={field.value} {...field} color="primary" />
<Switch checked={field.value!} {...field} color="primary" />
</StyledBox>
)}
/>
@@ -5,11 +5,11 @@ export type ProxyEmptyStateReason =
| 'no-proxy-info'
type ProfileSummary = {
current?: string
current?: string | null
items?: Array<{
uid?: string
type?: string
}>
uid?: string | null
type?: string | null
}> | null
}
interface ProxyListStateInput {
@@ -20,7 +20,7 @@ interface ProxyListStateInput {
isRunningModePending: boolean
}
const isSubscriptionProfile = (item: { type?: string }): boolean =>
const isSubscriptionProfile = (item: { type?: string | null }): boolean =>
item.type === 'remote' || item.type === 'local'
/** Decides only pre-render states; the built list owns whether it has renderable content. */
+2 -2
View File
@@ -198,7 +198,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
{toolsOnLeft && proxyCount}
<ProxyGroupTools
side={toolsOnLeft ? 'left' : 'right'}
url={group.testUrl}
url={group.testUrl!}
groupName={group.name}
headState={headState!}
onLocation={() => onLocation(group)}
@@ -247,7 +247,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
return (
<ProxyHead
sx={{ pl: 2, pr: 3, mt: 0.5, mb: 1 }}
url={group.testUrl}
url={group.testUrl!}
groupName={group.name}
headState={headState!}
onLocation={() => onLocation(group)}
+6 -6
View File
@@ -237,7 +237,7 @@ export const useRenderList = (
selected?.name ?? CHAIN_DELAY_GROUP,
'',
0,
latencyTimeout,
latencyTimeout!,
)
if (col > 1) {
return groupOccurrences(occurrences, col).map((memberCol) => ({
@@ -289,8 +289,8 @@ export const useRenderList = (
key: group.name,
group,
headState,
icon: group.icon,
testUrl: group.testUrl,
icon: group.icon!,
testUrl: group.testUrl!,
},
]
@@ -300,7 +300,7 @@ export const useRenderList = (
group.name,
headState.filterText,
headState.sortType,
latencyTimeout,
latencyTimeout!,
{
matchCase: headState.filterMatchCase,
matchWholeWord: headState.filterMatchWholeWord,
@@ -337,11 +337,11 @@ export const useRenderList = (
}
cache.set(group.name, {
now: group.now,
now: group.now!,
members: group.members,
headState,
col,
latencyTimeout,
latencyTimeout: latencyTimeout!,
delays: groupDelays.get(group.name),
items: ret,
})
@@ -50,9 +50,9 @@ export const BackupConfigViewer = memo(
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
defaultValues: {
url: webdav_url,
username: webdav_username,
password: webdav_password,
url: webdav_url!,
username: webdav_username!,
password: webdav_password!,
},
})
const url = watch('url')
+1 -1
View File
@@ -196,7 +196,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { verge } = useVerge()
const { current: currentProfile } = useProfiles()
const dnsEnabled = currentProfile
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
verge?.enable_dns_settings ??
false)
: false
@@ -438,7 +438,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<Switch
edge="end"
disabled={!enabled}
checked={value.pac}
checked={value.pac!}
onChange={(_, e) => setValue((v) => ({ ...v, pac: e }))}
/>
</ListItem>
@@ -455,7 +455,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<Switch
edge="end"
disabled={!enabled}
checked={value.guard}
checked={value.guard!}
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
sx={{ marginLeft: 'auto' }}
/>
@@ -1,5 +1,4 @@
import { alpha, Box, Button, LinearProgress } from '@mui/material'
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
import { useLockFn } from 'ahooks'
import type { Ref } from 'react'
import {
@@ -15,6 +14,7 @@ import type { Options as ReactMarkdownOptions } from 'react-markdown'
import { BaseDialog, DialogRef } from '@/components/base'
import { useUpdate } from '@/hooks/use-update'
import type { DownloadEvent } from '@/services/bindings'
import {
cancelUpdateDownload,
installUpdate,
+1 -1
View File
@@ -40,7 +40,7 @@ const SettingClash = ({ onError }: Props) => {
const { verge, mutateVerge } = useVerge()
const { current: currentProfile } = useProfiles()
const dnsEnabled = currentProfile
? (verge?.profile_dns_settings?.[currentProfile.uid]?.enabled ??
? (verge?.profile_dns_settings?.[currentProfile.uid!]?.enabled ??
verge?.enable_dns_settings ??
false)
: false
+6 -6
View File
@@ -25,11 +25,11 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
const [position, setPosition] = useState({ left: 0, top: 0 })
const [delay, setDelay] = useState(-1)
const { uid, name, icon, url } = itemData
const iconCachePath = useIconCache({ icon, cacheKey: uid })
const iconCachePath = useIconCache({ icon: icon!, cacheKey: uid! })
const onDelay = useCallback(async () => {
setDelay(-2)
const result = await cmdTestDelay(url)
const result = await cmdTestDelay(url!)
setDelay(result)
}, [url])
@@ -41,7 +41,7 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
const onDelete = useLockFn(async () => {
setAnchorEl(null)
try {
removeTest(uid)
removeTest(uid!)
} catch (err: any) {
showNotice.error(err)
}
@@ -81,17 +81,17 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{icon.trim().startsWith('http') && (
<img
alt={name}
alt={name!}
src={iconCachePath === '' ? icon : iconCachePath}
height="40px"
/>
)}
{icon.trim().startsWith('data') && (
<img alt={name} src={icon} height="40px" />
<img alt={name!} src={icon!} height="40px" />
)}
{icon.trim().startsWith('<svg') && (
<img
alt={name}
alt={name!}
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(icon)}`}
height="40px"
/>
+2 -2
View File
@@ -166,10 +166,10 @@ export const useProxySelection = (options: ProxySelectionOptions = {}) => {
const handleProxyGroupChange = useCallback(
(
group: { name: string; now?: string; fixed?: string },
group: { name: string; now?: string | null; fixed?: string | null },
proxy: { name: string },
) => {
changeProxy(group.name, proxy.name, group.now, group.fixed)
changeProxy(group.name, proxy.name, group.now!, group.fixed!)
},
[changeProxy],
)
+12 -12
View File
@@ -117,7 +117,7 @@ const ProfilePage = () => {
} = useProfiles()
useEffect(() => {
currentProfileRef.current = profiles.current
currentProfileRef.current = profiles.current!
}, [profiles])
useEffect(() => {
@@ -574,7 +574,7 @@ const ProfilePage = () => {
const onUpdateAll = useLockFn(async () => {
const items = profileItems.filter((e) => e.type === 'remote')
const target = items
.map((item) => item.uid)
.map((item) => item.uid!)
.filter((uid) => !loadingCache.has(uid))
setLoadingProfiles(target, true)
@@ -606,7 +606,7 @@ const ProfilePage = () => {
}
const selectAllProfiles = () => {
setSelectedProfiles(new Set(profileItems.map((item) => item.uid)))
setSelectedProfiles(new Set(profileItems.map((item) => item.uid!)))
}
const clearAllSelections = () => {
@@ -896,20 +896,20 @@ const ProfilePage = () => {
{profileItems.map((item, index) => (
<ProfileItem
key={item.uid}
id={item.uid}
id={item.uid!}
index={index}
selected={(switchTarget ?? profiles.current) === item.uid}
activating={
activatings.includes(item.uid) ||
activatings.includes(item.uid!) ||
visibleSwitchingProfile === item.uid
}
itemData={item}
timerUpdateRevision={timerRevisions.get(item.uid) ?? 0}
timerUpdateRevision={timerRevisions.get(item.uid!) ?? 0}
completedUpdateRevision={
completedUpdateRevisions.get(item.uid) ?? 0
completedUpdateRevisions.get(item.uid!) ?? 0
}
mutateProfiles={mutateProfiles}
onSelect={(f) => onSelect(item.uid, f)}
onSelect={(f) => onSelect(item.uid!, f)}
onEdit={() => viewerRef.current?.edit(item)}
onSave={async (prev, curr) => {
if (prev !== curr && profiles.current === item.uid) {
@@ -918,14 +918,14 @@ const ProfilePage = () => {
}}
onDelete={() => {
if (batchMode) {
toggleProfileSelection(item.uid)
toggleProfileSelection(item.uid!)
} else {
onDelete(item.uid)
onDelete(item.uid!)
}
}}
batchMode={batchMode}
isSelected={selectedProfiles.has(item.uid)}
onSelectionChange={() => toggleProfileSelection(item.uid)}
isSelected={selectedProfiles.has(item.uid!)}
onSelectionChange={() => toggleProfileSelection(item.uid!)}
/>
))}
</Box>
File diff suppressed because it is too large Load Diff
+132 -257
View File
@@ -1,141 +1,148 @@
import { Channel, invoke } from '@tauri-apps/api/core'
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
import { Channel } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import type { CommandFailure } from '@/services/notice-service'
import { showNotice } from '@/services/notice-service'
import type { ProxyViewV1 } from '@/types/proxy-view'
import { debugLog } from '@/utils/debug'
import { commands } from './bindings'
import type {
DownloadEvent,
JsonValue,
ListenerProbe,
ProxyPortSettings,
UnlockItem,
ProxyViewV1_Serialize as ProxyViewV1,
IProfiles_Deserialize,
PrfItem_Deserialize,
} from './bindings'
export type {
RunningMode,
RunStateView as RunState,
FailedOperation,
PendingFailure,
SidecarFailureSnapshot,
ServiceInstallOutcome,
ProxyPortSettings,
ListenerTransport,
ListenerProbe,
UnlockItem,
} from './bindings'
export async function copyClashEnv() {
return invoke<void>('copy_clash_env')
return commands.copyClashEnv()
}
export async function getProfiles() {
return invoke<IProfilesConfig>('get_profiles')
export async function getProfiles(): Promise<IProfilesConfig> {
return commands.getProfiles()
}
export async function enhanceProfiles() {
return (
(await invoke<ValidationOutcome>('enhance_profiles')).status === 'valid'
)
return (await commands.enhanceProfiles()).status === 'valid'
}
export async function patchProfilesConfig(profiles: IProfilesConfig) {
return invoke<ValidationOutcome>('patch_profiles_config', { profiles })
export async function patchProfilesConfig(profiles: IProfiles_Deserialize) {
return commands.patchProfilesConfig(profiles)
}
export async function createProfile(
item: Partial<IProfileItem>,
item: PrfItem_Deserialize,
fileData?: string | null,
) {
return invoke<void>('create_profile', { item, fileData })
return commands.createProfile(item, fileData ?? null)
}
export async function viewProfile(index: string) {
return invoke<void>('view_profile', { index })
return commands.viewProfile(index)
}
export async function readProfileFile(index: string) {
return invoke<string>('read_profile_file', { index })
return commands.readProfileFile(index)
}
export async function saveProfileFile(index: string, fileData: string) {
return (
(
await invoke<ValidationOutcome>('save_profile_file', {
index,
fileData,
})
).status === 'valid'
)
return (await commands.saveProfileFile(index, fileData)).status === 'valid'
}
export async function importProfile(url: string, option?: IProfileOption) {
return invoke<void>('import_profile', {
url,
option: option || { with_proxy: true },
})
return commands.importProfile(url, option || { with_proxy: true })
}
export async function reorderProfile(activeId: string, overId: string) {
return invoke<void>('reorder_profile', {
activeId,
overId,
})
return commands.reorderProfile(activeId, overId)
}
export async function updateProfile(index: string, option?: IProfileOption) {
return invoke<void>('update_profile', { index, option })
return commands.updateProfile(index, option ?? null)
}
export async function deleteProfile(index: string) {
return invoke<void>('delete_profile', { index })
return commands.deleteProfile(index)
}
export async function patchProfile(
index: string,
profile: Partial<IProfileItem>,
profile: PrfItem_Deserialize,
) {
return invoke<void>('patch_profile', { index, profile })
return commands.patchProfile(index, profile)
}
export async function getClashInfo() {
return invoke<IClashInfo | null>('get_clash_info')
return commands.getClashInfo()
}
// Fallback mode read independent of strict mihomo `/configs` deserialization.
export async function getClashMode() {
return invoke<string | null>('get_clash_mode')
return commands.getClashMode()
}
export async function getRuntimeConfig() {
return invoke<IConfigData | null>('get_runtime_config')
return (await commands.getRuntimeConfig()) as IConfigData | null
}
export async function getRuntimeYaml() {
return invoke<string | null>('get_runtime_yaml')
return commands.getRuntimeYaml()
}
export async function getRuntimeLogs() {
return invoke<Record<string, [string, string][]>>('get_runtime_logs')
return commands.getRuntimeLogs()
}
export async function getRuntimeProxyChainConfig(proxyChainExitNode: string) {
return invoke<string>('get_runtime_proxy_chain_config', {
proxyChainExitNode,
})
return commands.getRuntimeProxyChainConfig(proxyChainExitNode)
}
export async function updateProxyChainConfigInRuntime(proxyChainConfig: any) {
return invoke<void>('update_proxy_chain_config_in_runtime', {
proxyChainConfig,
})
export async function updateProxyChainConfigInRuntime(
proxyChainConfig: unknown,
) {
return commands.updateProxyChainConfigInRuntime(
proxyChainConfig == null ? null : (proxyChainConfig as JsonValue),
)
}
export async function patchClashConfig(payload: Partial<IConfigData>) {
return invoke<void>('patch_clash_config', { payload })
return commands.patchClashConfig(payload as Record<string, JsonValue>)
}
export async function patchClashMode(payload: string) {
return invoke<void>('patch_clash_mode', { payload })
return commands.patchClashMode(payload)
}
export async function syncTrayProxySelection() {
return invoke<void>('sync_tray_proxy_selection')
return commands.syncTrayProxySelection()
}
/** Sends one selection pair so the backend can merge against current profile state. */
export async function recordSelectedNode(groupName: string, node: string) {
return invoke<void>('record_selected_node', { groupName, node })
return commands.recordSelectedNode(groupName, node)
}
export async function forgetSelectedNode(groupName: string) {
return invoke<void>('forget_selected_node', { groupName })
return commands.forgetSelectedNode(groupName)
}
export async function getProxyView(): Promise<ProxyViewV1> {
const view = await invoke<ProxyViewV1>('get_proxy_view')
const view = await commands.getProxyView()
if (view.schemaVersion !== 1) {
throw new Error('Unsupported proxy view schema: ' + view.schemaVersion)
}
@@ -145,7 +152,7 @@ export async function getProxyView(): Promise<ProxyViewV1> {
export async function getClashLogs() {
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/
const newRegex = /(.+?)\s+(.+?)\s+(.+)/
const logs = await invoke<string[]>('get_clash_logs')
const logs = await commands.getClashLogs()
return logs.reduce<ILogItem[]>((acc, log) => {
const result = log.match(regex)
@@ -165,12 +172,12 @@ export async function getClashLogs() {
}, [])
}
export async function getVergeConfig() {
return invoke<IVergeConfig>('get_verge_config')
export async function getVergeConfig(): Promise<IVergeConfig> {
return commands.getVergeConfig()
}
export async function patchVergeConfig(payload: IVergeConfig) {
return invoke<void>('patch_verge_config', { payload })
return commands.patchVergeConfig(payload)
}
export async function setDnsOverride(
@@ -178,76 +185,57 @@ export async function setDnsOverride(
enabled: boolean,
confirmation?: string,
) {
return invoke<
{ status: 'applied' } | { status: 'confirmation_required'; source: string }
>('set_dns_override', { profileUid, enabled, confirmation })
return commands.setDnsOverride(profileUid, enabled, confirmation ?? null)
}
export async function getDnsConfigContent() {
return invoke<string | null>('get_dns_config_content')
return commands.getDnsConfigContent()
}
export async function saveDnsConfig(dnsConfig: Record<string, unknown>) {
return invoke<void>('save_dns_config', { dnsConfig })
return commands.saveDnsConfig(dnsConfig as Record<string, JsonValue>)
}
export async function validateDnsConfig() {
return invoke<ValidationOutcome>('validate_dns_config')
return commands.validateDnsConfig()
}
export async function applyDnsConfig(apply: boolean) {
return invoke<void>('apply_dns_config', { apply })
return commands.applyDnsConfig(apply)
}
export async function takeDnsOverrideNotice() {
return invoke<boolean>('take_dns_override_notice')
return commands.takeDnsOverrideNotice()
}
export type ServiceFallbackNotice =
| { kind: 'unavailable' }
| { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
export async function takeServiceFallbackNotice() {
return invoke<ServiceFallbackNotice | null>('take_service_fallback_notice')
}
export interface CoreFailure {
kind: 'startFailed' | 'serviceCoreStopped'
detail: string
return commands.takeServiceFallbackNotice()
}
export async function getCoreStartupError() {
return invoke<CoreFailure | null>('get_core_startup_error')
return commands.getCoreStartupError()
}
export async function takeServiceRepairNotice() {
return invoke<boolean>('take_service_repair_notice')
return commands.takeServiceRepairNotice()
}
export async function takeServiceOwnerNotice() {
return invoke<string | null>('take_service_owner_notice')
return commands.takeServiceOwnerNotice()
}
export async function takeDiscardedKeysNotice() {
return invoke<string | null>('take_discarded_keys_notice')
return commands.takeDiscardedKeysNotice()
}
export async function getSystemProxy() {
return invoke<{
enable: boolean
server: string
bypass: string
}>('get_sys_proxy')
return commands.getSysProxy()
}
export async function getAutotemProxy() {
try {
debugLog('[API] 开始调用 get_auto_proxy')
const result = await invoke<{
enable: boolean
url: string
}>('get_auto_proxy')
const result = await commands.getAutoProxy()
debugLog('[API] get_auto_proxy 调用成功:', result)
return result
} catch (error) {
@@ -260,30 +248,23 @@ export async function getAutotemProxy() {
}
export async function getEmbeddedServerPort() {
return invoke<number>('get_embedded_server_port')
return commands.getEmbeddedServerPort()
}
export async function changeClashCore(clashCore: string) {
return invoke<CommandFailure | null>('change_clash_core', { clashCore })
return commands.changeClashCore(clashCore)
}
export async function restartCore() {
return invoke<void>('restart_core')
}
export interface CoreUpgradeReport {
/** False when the managed core was already at the latest version. */
upgraded: boolean
from: string
to: string
return commands.restartCore()
}
export async function upgradeClashCore(force = false) {
return invoke<CoreUpgradeReport>('upgrade_clash_core', { force })
return commands.upgradeClashCore(force)
}
export async function restartApp() {
return invoke<void>('restart_app')
return commands.restartApp()
}
export async function installUpdate(
@@ -292,69 +273,57 @@ export async function installUpdate(
) {
const channel = new Channel<DownloadEvent>()
channel.onmessage = onEvent
return invoke<boolean>('install_update', { version, onEvent: channel })
return commands.installUpdate(version, channel)
}
export async function cancelUpdateDownload() {
return invoke<void>('cancel_update_download')
return commands.cancelUpdateDownload()
}
export async function getAppDir() {
return invoke<string>('get_app_dir')
return commands.getAppDir()
}
export async function openAppDir() {
return invoke<void>('open_app_dir').catch((err) => showNotice.error(err))
return commands.openAppDir().catch((err) => showNotice.error(err))
}
export async function openCoreDir() {
return invoke<void>('open_core_dir').catch((err) => showNotice.error(err))
return commands.openCoreDir().catch((err) => showNotice.error(err))
}
export async function openLogsDir() {
return invoke<void>('open_logs_dir').catch((err) => showNotice.error(err))
return commands.openLogsDir().catch((err) => showNotice.error(err))
}
export async function syncRuntimeProviders() {
return invoke<void>('sync_runtime_providers').catch((err) => {
return commands.syncRuntimeProviders().catch((err) => {
console.warn('failed to queue the provider cache sync', err)
})
}
export async function cmdTestDelay(url: string) {
return invoke<number>('test_delay', { url })
return commands.testDelay(url)
}
export async function invoke_uwp_tool() {
return invoke<void>('invoke_uwp_tool').catch((err) =>
showNotice.error(err, 1500),
)
return commands.invokeUwpTool().catch((err) => showNotice.error(err, 1500))
}
export async function openDevTools() {
return invoke('open_devtools')
return commands.openDevtools()
}
export async function exitApp() {
return invoke('exit_app')
return commands.exitApp()
}
export async function exportDiagnosticInfo() {
return invoke('export_diagnostic_info')
}
interface SystemInfo {
system_name: string
system_version: string
system_kernel_version: string
system_arch: string
app_version: string
app_core_mode: string
app_is_admin: boolean
return commands.exportDiagnosticInfo()
}
export async function getSystemInfo() {
return invoke<SystemInfo>('get_system_info')
return commands.getSystemInfo()
}
export async function copyIconFile(
@@ -373,55 +342,55 @@ export async function copyIconFile(
current_t: currentTime,
}
return invoke<void>('copy_icon_file', { path, iconInfo })
return commands.copyIconFile(path, iconInfo)
}
export async function downloadIconCache(url: string, name: string) {
return invoke<string>('download_icon_cache', { url, name })
return commands.downloadIconCache(url, name)
}
export async function getNetworkInterfaces() {
return invoke<string[]>('get_network_interfaces')
return commands.getNetworkInterfaces()
}
export async function getSystemHostname() {
return invoke<string>('get_system_hostname')
return commands.getSystemHostname()
}
export async function getNetworkInterfacesInfo() {
return invoke<INetworkInterface[]>('get_network_interfaces_info')
return commands.getNetworkInterfacesInfo()
}
export async function createWebdavBackup() {
return invoke<void>('create_webdav_backup')
return commands.createWebdavBackup()
}
export async function createLocalBackup() {
return invoke<void>('create_local_backup')
return commands.createLocalBackup()
}
export async function deleteWebdavBackup(filename: string) {
return invoke<void>('delete_webdav_backup', { filename })
return commands.deleteWebdavBackup(filename)
}
export async function deleteLocalBackup(filename: string) {
return invoke<void>('delete_local_backup', { filename })
return commands.deleteLocalBackup(filename)
}
export async function restoreWebDavBackup(filename: string) {
return invoke<void>('restore_webdav_backup', { filename })
return commands.restoreWebdavBackup(filename)
}
export async function restoreLocalBackup(filename: string) {
return invoke<void>('restore_local_backup', { filename })
return commands.restoreLocalBackup(filename)
}
export async function importLocalBackup(source: string) {
return invoke<string>('import_local_backup', { source })
return commands.importLocalBackup(source)
}
export async function exportLocalBackup(filename: string, destination: string) {
return invoke<void>('export_local_backup', { filename, destination })
return commands.exportLocalBackup(filename, destination)
}
export async function saveWebdavConfig(
@@ -429,177 +398,83 @@ export async function saveWebdavConfig(
username: string,
password: string,
) {
return invoke<void>('save_webdav_config', {
url,
username,
password,
})
return commands.saveWebdavConfig(url, username, password)
}
export async function listWebDavBackup() {
const list: IWebDavFile[] = await invoke<IWebDavFile[]>('list_webdav_backup')
list.forEach((item) => {
item.filename = item.href.split('/').pop() as string
})
return list
return (await commands.listWebdavBackup()).map((item) => ({
...item,
filename: item.href.split('/').pop() ?? '',
tag: item.tag,
}))
}
export async function listLocalBackup() {
return invoke<ILocalBackupFile[]>('list_local_backup')
return commands.listLocalBackup()
}
export type RunningMode = 'Service' | 'Sidecar' | 'NotRunning'
type ServiceHealth =
| 'unknown'
| 'ready'
| 'notInstalled'
| 'versionMismatch'
| 'unavailable'
type PendingServiceAction =
| 'install'
| 'uninstall'
| 'reinstall'
| 'forceReinstall'
/** Consistent core/service snapshot with backend-derived availability flags. */
export interface RunState {
mode: RunningMode
service: ServiceHealth
serviceUnavailableReason: string | null
pendingAction: PendingServiceAction | null
sidecarAllowed: boolean
isAdmin: boolean
opInFlight: boolean
serviceUsable: boolean
tunCapable: boolean
serviceNeedsAttention: boolean
}
export const getRuntimeState = async () => {
return invoke<RunState>('get_runtime_state')
}
export type FailedOperation =
| 'systemProxyEnable'
| 'systemProxyDisable'
| 'systemProxyRestore'
| 'systemProxyGuard'
export interface PendingFailure {
code: string
detail: string
operation: FailedOperation
sequence: number
return commands.getRuntimeState()
}
export const getPendingFailures = async () => {
return invoke<PendingFailure[]>('get_pending_failures')
}
export interface SidecarFailureSnapshot {
revision: number
detail: string | null
return commands.getPendingFailures()
}
export const getSidecarFailure = async () => {
return invoke<SidecarFailureSnapshot>('get_sidecar_failure')
return commands.getSidecarFailure()
}
export const getAppUptime = async () => {
return invoke<number>('get_app_uptime')
return commands.getAppUptime()
}
export type ServiceInstallOutcome =
| { status: 'installed' }
| { status: 'sidecar'; reason: string }
export const installService = async () => {
return invoke<ServiceInstallOutcome>('install_service')
return commands.installService()
}
export const uninstallService = async () => {
return invoke<void>('uninstall_service')
return commands.uninstallService()
}
export const reinstallService = async () => {
return invoke<ServiceInstallOutcome>('reinstall_service')
return commands.reinstallService()
}
export const repairService = async () => {
return invoke<ServiceInstallOutcome>('repair_service')
return commands.repairService()
}
export const continueWithSidecar = async () => {
return invoke<void>('continue_with_sidecar')
return commands.continueWithSidecar()
}
export const entry_lightweight_mode = async () => {
return invoke<void>('entry_lightweight_mode')
return commands.entryLightweightMode()
}
export async function getNextUpdateTime(uid: string) {
return invoke<number | null>('get_next_update_time', { uid })
return commands.getNextUpdateTime(uid)
}
interface ToggleableProxyPort {
enabled: boolean
port: number
}
export interface ProxyPortSettings {
mixedPort: number
socks: ToggleableProxyPort
http: ToggleableProxyPort
redir: ToggleableProxyPort
tproxy: ToggleableProxyPort
}
export type ListenerTransport = 'tcp' | 'udp'
export interface ListenerProbe {
address: string
transports: ListenerTransport[]
}
export type ListenerProbeOutcome =
| { status: 'available' }
| {
status: 'conflict'
port: number
transport: ListenerTransport
}
| { status: 'invalid'; message: string }
| { status: 'indeterminate'; message: string }
export type SaveProxyPortsOutcome =
| { status: 'saved' }
| { status: 'conflict'; port: number; transport: ListenerTransport }
export const probeListener = async (request: ListenerProbe) => {
return invoke<ListenerProbeOutcome>('probe_listener', { request })
return commands.probeListener(request)
}
export const saveProxyPorts = async (settings: ProxyPortSettings) => {
return invoke<SaveProxyPortsOutcome>('save_proxy_ports', { settings })
}
export interface UnlockItem {
name: string
status: string
region?: string | null
check_time?: string | null
return commands.saveProxyPorts(settings)
}
export async function getUnlockItems() {
return invoke<UnlockItem[]>('get_unlock_items')
return commands.getUnlockItems()
}
export async function checkMediaUnlock(onComplete: Channel<UnlockItem>) {
return invoke<UnlockItem[]>('check_media_unlock', { onComplete })
return commands.checkMediaUnlock(onComplete)
}
export async function checkMediaUnlockItem(name: string) {
return invoke<UnlockItem>('check_media_unlock_item', { name })
return commands.checkMediaUnlockItem(name)
}
+1 -56
View File
@@ -1,56 +1 @@
// @generated by scripts/gen-contract.mjs from the backend wire contract. DO NOT EDIT.
// Regen: pnpm gen:contract
import type { RunState } from './cmds'
/** Wire status strings from NoticeStatus; keep in sync via `pnpm gen:contract`. */
export type NoticeStatus =
| 'info'
| 'import_sub_url::ok'
| 'import_sub_url::error'
| 'set_config::error'
| 'core_start::error'
| 'service_core::repair_required'
| 'service_core::app_data_not_owned'
| 'service_core::sidecar_fallback'
| 'dns_override::auto_disabled'
| 'enhance::discarded_keys'
| 'tun_mode::auto_disabled'
| 'tun_mode::auto_disable_failed'
| 'app_restart::core_stop_failed'
| 'app_quit::core_stop_failed'
| 'update_with_clash_proxy'
| 'update_failed_even_with_clash'
| 'reactivate_profiles::error'
| 'update_failed'
| 'config_validate::boot_error'
| 'config_validate::error'
| 'config_validate::process_terminated'
| 'config_validate::script_error'
| 'config_validate::script_syntax_error'
| 'config_validate::script_missing_main'
| 'config_validate::file_not_found'
| 'config_validate::yaml_syntax_error'
| 'config_validate::yaml_read_error'
| 'config_validate::yaml_mapping_error'
| 'config_validate::merge_syntax_error'
| 'config_validate::merge_mapping_error'
| 'config_core::change_success'
| 'config_core::change_error'
| 'mixed_port::fallback'
| 'mixed_port::fallback_error'
/** Event name → payload type for every backend-emitted event. */
export interface VergeEventPayloads {
'verge://refresh-clash-config': string
'verge://refresh-verge-config': string
'verge://refresh-profiles': string
'verge://refresh-proxy-config': null
'verge://notice-message': [NoticeStatus, string]
'verge://timer-updated': string
'verge://run-state-changed': RunState
'verge://pending-failures-changed': null
'profile-changed': string
'profile-update-started': { uid: string }
'profile-update-completed': { uid: string }
}
export type { NoticeStatus, VergeEventPayloads } from './bindings'
+3 -7
View File
@@ -305,11 +305,7 @@ function extractDisplayText(input: unknown): string | undefined {
}
}
export interface CommandFailure {
code?: string
operation?: FailedOperation
detail: string
}
import type { CommandFailure_Serialize as CommandFailure } from './bindings'
function isCommandFailure(input: unknown): input is CommandFailure {
if (typeof input !== 'object' || input === null) return false
@@ -495,11 +491,11 @@ const baseShowNotice = (
}
function failureCode(input: unknown): string | undefined {
return isCommandFailure(input) ? input.code : undefined
return isCommandFailure(input) ? input.code! : undefined
}
function failureOperation(input: unknown): FailedOperation | undefined {
return isCommandFailure(input) ? input.operation : undefined
return isCommandFailure(input) ? input.operation! : undefined
}
/**
+23 -184
View File
@@ -13,10 +13,7 @@ type Platform =
declare const OS_PLATFORM: Platform
type ValidationOutcome =
| { status: 'valid' | 'busy' }
| { status: 'invalid'; kind: string; message: string }
| { status: 'skipped'; reason: string }
type ValidationOutcome = import('@/services/bindings').ValidationOutcome
interface IConfigData {
port: number
@@ -207,82 +204,19 @@ interface IConnectionSetting {
layout: 'table' | 'list'
}
interface IClashInfo {
mixed_port?: number // clash mixed port
socks_port?: number // clash socks port
redir_port?: number // clash redir port
tproxy_port?: number // clash tproxy port
port?: number // clash http port
server?: string // external-controller
secret?: string
}
type IClashInfo = import('@/services/bindings').ClashInfo
interface IProfileItem {
uid: string
type?: 'local' | 'remote' | 'merge' | 'script'
name?: string
desc?: string
file?: string
url?: string
updated?: number
selected?: {
name?: string
now?: string
}[]
extra?: {
upload: number
download: number
total: number
expire: number
}
option?: IProfileOption
home?: string
}
type IProfileItem = import('@/services/bindings').PrfItem_Serialize
interface IProfileOption {
user_agent?: string
with_proxy?: boolean
self_proxy?: boolean
update_interval?: number
timeout_seconds?: number
danger_accept_invalid_certs?: boolean
allow_auto_update?: boolean
merge?: string
script?: string
rules?: string
proxies?: string
groups?: string
}
type IProfileOption = import('@/services/bindings').PrfOption_Deserialize
interface IProfilesConfig {
current?: string
items?: IProfileItem[]
}
type IProfilesConfig = Partial<
import('@/services/bindings').IProfiles_Serialize
>
interface IVergeTestItem {
uid: string
name?: string
icon?: string
url: string
}
interface IAddress {
V4?: {
ip: string
broadcast?: string
netmask?: string
}
V6?: {
ip: string
broadcast?: string
netmask?: string
}
}
interface INetworkInterface {
name: string
addr: IAddress[]
mac_addr?: string
index: number
}
type IVergeTestItem = import('@/services/bindings').IVergeTestItem
type IAddress = import('@/services/bindings').InterfaceAddress
type INetworkInterface = import('@/services/bindings').NetworkInterfaceView
interface ISeqProfileConfig {
prepend: []
@@ -850,119 +784,24 @@ interface IProxyConfig
| 'sudoku'
}
interface IVergeConfig {
app_log_level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | string
app_log_max_size?: number // KB
app_log_max_count?: number
language?: string
tray_event?:
| 'main_window'
| 'tray_menu'
| 'system_proxy'
| 'tun_mode'
| string
env_type?: 'bash' | 'cmd' | 'powershell' | 'fish' | string
startup_script?: string
start_page?: string
clash_core?: string
theme_mode?: 'light' | 'dark' | 'system'
traffic_graph?: boolean
enable_memory_usage?: boolean
enable_group_icon?: boolean
pause_render_traffic_stats_on_blur?: boolean
menu_icon?: 'monochrome' | 'colorful' | 'disable'
menu_order?: string[]
proxy_group_tools_position?: 'left' | 'right'
notice_position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
collapse_navbar?: boolean
tray_icon?: 'monochrome' | 'colorful'
common_tray_icon?: boolean
sysproxy_tray_icon?: boolean
tun_tray_icon?: boolean
enable_tray_speed?: boolean
tray_proxy_groups_display_mode?: 'default' | 'inline' | 'disable'
tray_inline_outbound_modes?: boolean
enable_tun_mode?: boolean
enable_auto_light_weight_mode?: boolean
auto_light_weight_minutes?: number
enable_auto_launch?: boolean
enable_silent_start?: boolean
enable_system_proxy?: boolean
enable_global_hotkey?: boolean
enable_dns_settings?: boolean
profile_dns_settings?: Record<string, { enabled: boolean }>
proxy_auto_config?: boolean
pac_file_content?: string
proxy_host?: string
enable_random_port?: boolean
verge_mixed_port?: number
verge_socks_port?: number
verge_redir_port?: number
verge_tproxy_port?: number
verge_port?: number
verge_redir_enabled?: boolean
verge_tproxy_enabled?: boolean
verge_socks_enabled?: boolean
verge_http_enabled?: boolean
enable_proxy_guard?: boolean
enable_bypass_check?: boolean
use_default_bypass?: boolean
proxy_guard_duration?: number
system_proxy_bypass?: string
web_ui_list?: string[]
hotkeys?: string[]
theme_setting?: {
primary_color?: string
secondary_color?: string
primary_text?: string
secondary_text?: string
info_color?: string
error_color?: string
warning_color?: string
success_color?: string
font_family?: string
css_injection?: string
background_image?: string
background_blend_mode?: string
background_opacity?: number
}
auto_close_connection?: boolean
auto_check_update?: boolean
default_latency_test?: string
default_latency_timeout?: number
enable_auto_delay_detection?: boolean
auto_delay_detection_interval_minutes?: number
enable_builtin_enhanced?: boolean
auto_log_clean?: 0 | 1 | 2 | 3 | 4
enable_auto_backup_schedule?: boolean
auto_backup_interval_hours?: number
auto_backup_on_change?: boolean
proxy_layout_column?: number
test_list?: IVergeTestItem[]
webdav_url?: string
webdav_username?: string
webdav_password?: string
home_cards?: Record<string, boolean>
enable_hover_jump_navigator?: boolean
hover_jump_navigator_delay?: number
enable_external_controller?: boolean
type IVergeConfig = Omit<
Partial<import('@/services/bindings').IVerge_Serialize>,
'theme_setting'
> & {
theme_setting?:
| (import('@/services/bindings').IVergeTheme & {
background_image?: string
background_blend_mode?: string
background_opacity?: number
})
| null
}
interface IWebDavFile {
type IWebDavFile = import('@/services/bindings').WebDavFile & {
filename: string
href: string
last_modified: string
content_length: number
content_type: string
tag: string
}
interface ILocalBackupFile {
filename: string
path: string
last_modified: string
content_length: number
}
type ILocalBackupFile = import('@/services/bindings').LocalBackupFile
interface IWebDavConfig {
url: string
+12 -81
View File
@@ -1,84 +1,15 @@
export interface ProxyViewV1 {
schemaVersion: 1
orderSource: 'runtime' | 'fallback'
providerState: 'ready' | 'unavailable'
global: ProxyGroupView | null
direct: string | null
groups: ProxyGroupView[]
records: Record<string, ProxyNodeView>
standalone: string[]
providers: ProxyProviderView[]
}
interface ProxyCapabilities {
udp: boolean
xudp: boolean
tfo: boolean
mptcp: boolean
smux: boolean
}
interface DelayHistory {
time: string
delay: number
}
export interface ProxyGroupView extends ProxyCapabilities {
name: string
type: string
alive: boolean
now?: string
fixed?: string
hidden?: boolean
icon?: string
testUrl?: string
history: DelayHistory[]
members: ProxyMemberRef[]
}
export interface ProxyNodeView extends ProxyCapabilities {
recordId: string
name: string
type: string
alive: boolean
history: DelayHistory[]
id?: string
hidden?: boolean
icon?: string
testUrl?: string
source:
| { kind: 'core'; proxyName: string }
| { kind: 'provider'; providerName: string; proxyName: string }
}
type ProxyMemberUnresolvedReason =
| 'missing'
| 'ambiguous'
| 'provider-unavailable'
export type ProxyMemberRef =
| { kind: 'group'; name: string }
| { kind: 'node'; name: string; recordId: string }
| {
kind: 'unresolved'
name: string
reason: ProxyMemberUnresolvedReason
}
interface ProxyProviderView {
name: string
vehicleType: 'HTTP' | 'File' | 'Inline'
updatedAt?: string
subscriptionInfo?: ProxySubscriptionInfo
proxyRecordIds: string[]
}
interface ProxySubscriptionInfo {
upload: number
download: number
total: number
expire: number
}
import type {
ProxyViewV1_Serialize as ProxyViewV1,
ProxyGroupView_Serialize as ProxyGroupView,
ProxyNodeView_Serialize as ProxyNodeView,
ProxyMemberRef,
} from '@/services/bindings'
export type {
ProxyViewV1_Serialize as ProxyViewV1,
ProxyGroupView_Serialize as ProxyGroupView,
ProxyNodeView_Serialize as ProxyNodeView,
ProxyMemberRef,
} from '@/services/bindings'
export type ResolvedProxyMember =
| {