From 9a879dd8d869f1497a65d9c2c9d2f9e1ba65778d Mon Sep 17 00:00:00 2001 From: wonfen Date: Wed, 16 Sep 2026 23:42:48 +0800 Subject: [PATCH] fix(service): fall back to elevated sidecar on path permission rejection Preserve TUN settings and restore proxy operation after fallback. Deliver fallback notices after UI subscription and stop retrying deterministic bundle refusals. Publish fallback state atomically and avoid conflicting service startup notifications. --- Changelog.md | 1 + src-tauri/src/cmd/service.rs | 5 ++ src-tauri/src/core/manager/lifecycle.rs | 61 +++++++++++++++- src-tauri/src/core/manager/state.rs | 71 ++++++++++++------- src-tauri/src/core/runstate/mod.rs | 15 ++++ src-tauri/src/core/service.rs | 41 +++++++++-- src-tauri/src/lib.rs | 1 + .../layout/sysproxy-privilege-dialog.tsx | 23 +++--- src/locales/ar/settings.json | 1 + src/locales/de/settings.json | 1 + src/locales/en/settings.json | 1 + src/locales/es/settings.json | 1 + src/locales/fa/settings.json | 1 + src/locales/id/settings.json | 1 + src/locales/jp/settings.json | 1 + src/locales/ko/settings.json | 1 + src/locales/ru/settings.json | 1 + src/locales/tr/settings.json | 1 + src/locales/tt/settings.json | 1 + src/locales/zh/settings.json | 1 + src/locales/zhtw/settings.json | 1 + .../_layout/hooks/use-layout-events.test.ts | 5 +- src/pages/_layout/hooks/use-layout-events.ts | 1 + .../_layout/utils/notification-handlers.ts | 21 +++++- src/services/cmds.ts | 4 ++ src/types/generated/i18n-keys.ts | 1 + src/types/generated/i18n-resources.ts | 1 + 27 files changed, 225 insertions(+), 39 deletions(-) diff --git a/Changelog.md b/Changelog.md index 695b0d1f3..a6c185b3f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -27,6 +27,7 @@ **🖥️ Windows** +- 修复 Windows 管理员运行时,服务权限校验失败后无法继续使用代理的问题 - 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题 - 修复 Windows 系统代理设置偶发失败的问题 - 修复 Windows 关机被应用阻止的问题 diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 1d1a21074..63eaed118 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -57,6 +57,11 @@ pub async fn continue_with_sidecar() -> CmdResult { .map_err(|error| proxy_aware_coded_error(&error, "SERVICE_SIDECAR_FAILED")) } +#[tauri::command] +pub fn take_service_fallback_notice() -> bool { + crate::core::service::take_service_fallback_notice() +} + #[tauri::command] pub fn sync_runtime_providers() { if matches!(*CoreManager::global().get_running_mode(), RunningMode::Service) { diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 0496859d2..c19f2d85b 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -185,6 +185,42 @@ where apply_proxy().await } +#[cfg(target_os = "windows")] +async fn run_service_start_with_sidecar_fallback( + is_admin: bool, + start_service: Start, + start_sidecar: Fallback, +) -> Result<()> +where + Start: FnOnce() -> StartFuture, + StartFuture: std::future::Future>, + Fallback: FnOnce(anyhow::Error) -> FallbackFuture, + FallbackFuture: std::future::Future>, +{ + let Err(error) = start_service().await else { + return Ok(()); + }; + // A transport failure cannot establish whether the service already started the core. + let location_refused = error + .downcast_ref::() + .is_some_and(|refusal| { + refusal.code == clash_verge_service_ipc::ServiceErrorCode::InvalidInstallLocation as u16 + }); + if !is_admin || !location_refused { + return Err(error); + } + + logging!( + warn, + Type::Core, + "service refused the core location while app is elevated; falling back to sidecar: {error:#}" + ); + let reason = format!("{error:#}"); + start_sidecar(error) + .await + .map_err(|error| error.context(format!("sidecar fallback failed after service refusal: {reason}"))) +} + async fn run_sidecar_termination_transition( clear_proxy: Clear, terminate_sidecar: Terminate, @@ -550,7 +586,30 @@ impl CoreManager { } let result = match startup { - StartupDecision::Service => self.start_core_by_service().await, + StartupDecision::Service => { + #[cfg(target_os = "windows")] + { + run_service_start_with_sidecar_fallback( + crate::core::runstate::RUN_STATE.state().is_admin, + || self.start_core_by_service(), + |error| async move { + use crate::core::runstate::RUN_STATE; + + RUN_STATE.allow_sidecar_after_service_refusal(format!("{error:#}"))?; + let result = self.start_core_by_sidecar().await; + if result.is_err() { + SERVICE_MANAGER.withdraw_sidecar_allowance(); + } else { + crate::core::service::notify_service_fallback(); + } + result + }, + ) + .await + } + #[cfg(not(target_os = "windows"))] + self.start_core_by_service().await + } StartupDecision::Sidecar => self.start_core_by_sidecar().await, StartupDecision::Wait => Ok(()), }; diff --git a/src-tauri/src/core/manager/state.rs b/src-tauri/src/core/manager/state.rs index d7486651f..996e062ef 100644 --- a/src-tauri/src/core/manager/state.rs +++ b/src-tauri/src/core/manager/state.rs @@ -18,6 +18,44 @@ use tauri_plugin_shell::ShellExt as _; const SIDECAR_READINESS_ATTEMPTS: usize = 30; +#[cfg(target_os = "windows")] +async fn retry_service_start( + attempts: usize, + retry_delay: std::time::Duration, + mut start: Start, +) -> Result<()> +where + Start: FnMut() -> StartFuture, + StartFuture: std::future::Future>, +{ + let mut last_error = None; + for attempt in 0..attempts { + match start().await { + Ok(()) => return Ok(()), + Err(error) => { + logging!( + warn, + Type::Core, + "service start attempt {}/{} failed: {error:#}", + attempt + 1, + attempts + ); + if error + .downcast_ref::() + .is_some_and(|refusal| service::StageRequest::is_about_the_bundle(refusal.code)) + { + return Err(error); + } + last_error = Some(error); + if attempt + 1 < attempts { + tokio::time::sleep(retry_delay).await; + } + } + } + } + Err(last_error.unwrap_or_else(|| anyhow::anyhow!("service start failed"))) +} + impl CoreManager { /// Restores profile selections before callers enable the system proxy. /// The bounded first pass continues in the background, and later calls supersede earlier ones. @@ -269,30 +307,15 @@ impl CoreManager { #[cfg(target_os = "windows")] { use crate::constants::timing; - let mut last_err = None; - for attempt in 0..timing::SERVICE_START_RETRIES { - match service::run_core_by_service(config_file).await { - Ok(()) => { - self.mark_core_ready(); - self.core_started(RunningMode::Service); - self.restore_selected_nodes().await; - service::request_runtime_provider_sync(crate::constants::timing::RUNTIME_PROVIDER_SYNC_DELAY); - return Ok(()); - } - Err(e) => { - logging!( - warn, - Type::Core, - "service start attempt {}/{} failed: {e:#}", - attempt + 1, - timing::SERVICE_START_RETRIES - ); - last_err = Some(e); - tokio::time::sleep(timing::SERVICE_START_RETRY_DELAY).await; - } - } - } - Err(last_err.unwrap_or_else(|| anyhow::anyhow!("service start failed"))) + retry_service_start(timing::SERVICE_START_RETRIES, timing::SERVICE_START_RETRY_DELAY, || { + service::run_core_by_service(config_file) + }) + .await?; + self.mark_core_ready(); + self.core_started(RunningMode::Service); + self.restore_selected_nodes().await; + service::request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SYNC_DELAY); + Ok(()) } #[cfg(not(target_os = "windows"))] diff --git a/src-tauri/src/core/runstate/mod.rs b/src-tauri/src/core/runstate/mod.rs index 69959873a..3153b4666 100644 --- a/src-tauri/src/core/runstate/mod.rs +++ b/src-tauri/src/core/runstate/mod.rs @@ -323,6 +323,21 @@ impl RunStateStore { Ok(()) } + #[cfg(target_os = "windows")] + pub fn allow_sidecar_after_service_refusal(&self, reason: String) -> Result<()> { + let mut state = self.service.lock(); + if self.operation_running.load(Ordering::Acquire) || state.service.pending.is_some() { + bail!("cannot fall back while a service operation is pending"); + } + // Publish the refusal and allowance together so it never asks the UI for a repair. + state.service.health = ServiceHealth::Unavailable(reason); + state.service.allow_sidecar(); + state.bump(); + drop(state); + self.announce(); + Ok(()) + } + /// Requests installation only for a confirmed absent Service. pub fn require_install_for_session(&self) -> Result<()> { let mut state = self.service.lock(); diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 7529cc6e2..6c9559ec2 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -39,6 +39,17 @@ use std::{ static OWNER_MONITOR_GENERATION: AtomicU64 = AtomicU64::new(0); static ACTIVE_SERVICE_SESSION: Lazy>> = Lazy::new(|| Mutex::new(None)); +static PENDING_SERVICE_FALLBACK_NOTICE: AtomicBool = AtomicBool::new(false); + +#[cfg(target_os = "windows")] +pub(crate) fn notify_service_fallback() { + PENDING_SERVICE_FALLBACK_NOTICE.store(true, Ordering::Relaxed); + Handle::notice_message("service_core::sidecar_fallback", ""); +} + +pub(crate) fn take_service_fallback_notice() -> bool { + PENDING_SERVICE_FALLBACK_NOTICE.swap(false, Ordering::Relaxed) +} /// Capabilities of the service session that owns the running Core. /// They are discarded with that session rather than cached across service upgrades. @@ -1076,6 +1087,25 @@ pub(super) async fn stage_runtime_by_service(config_file: &Path) -> Result) -> std::fmt::Result { + write!( + formatter, + "failed to start Service core at {} (code {}): {}", + self.core_path, self.code, self.message + ) + } +} + +impl std::error::Error for ServiceStartRefusal {} + /// 尝试使用服务启动core #[tracing::instrument(skip_all, level = "info", fields(generation = tracing::field::Empty, staging = tracing::field::Empty, code = tracing::field::Empty, outcome = tracing::field::Empty))] pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<()> { @@ -1111,10 +1141,12 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<() err_msg ); start_owner_monitor(); - bail!( - "failed to start Service core at {}: {err_msg}", - request.runtime.core_path - ); + return Err(ServiceStartRefusal { + code: response.code, + core_path: request.runtime.core_path, + message: err_msg, + } + .into()); } let result = response.data.context("Clash Verge Service 未返回会话信息")?; @@ -1133,6 +1165,7 @@ pub(super) async fn start_with_existing_service(config_file: &Path) -> Result<() // PAC follows the Running Mode; the caller opens it via `core_started(Service)`. start_owner_monitor(); tracing::Span::current().record("outcome", "started"); + PENDING_SERVICE_FALLBACK_NOTICE.store(false, Ordering::Relaxed); logging!( info, Type::Service, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 19891eba3..a1e4d1a82 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -163,6 +163,7 @@ mod app_init { cmd::apply_dns_config, cmd::set_dns_override, cmd::take_dns_override_notice, + cmd::take_service_fallback_notice, cmd::get_dns_config_content, cmd::validate_dns_config, cmd::get_clash_logs, diff --git a/src/components/layout/sysproxy-privilege-dialog.tsx b/src/components/layout/sysproxy-privilege-dialog.tsx index c193c4c59..e3f85665c 100644 --- a/src/components/layout/sysproxy-privilege-dialog.tsx +++ b/src/components/layout/sysproxy-privilege-dialog.tsx @@ -20,6 +20,7 @@ import { type ServiceRequest, type ServiceRequestReason, } from '@/services/service-request' +import getSystem from '@/utils/get-system' type Remedy = 'installAndRestart' | 'restartOnly' @@ -98,19 +99,25 @@ export const SysproxyPrivilegeDialog = () => { setStep('restarting') await restartCore() - // Verify that restart actually moved the core into the service. const runState = await getRuntimeState() - if (runState.mode === 'Service') { - // Retry the original request now that the service can apply it. + const usingAdminFallback = + getSystem() === 'windows' && + runState.mode === 'Sidecar' && + runState.isAdmin && + runState.sidecarAllowed && + runState.service === 'unavailable' + if (runState.mode === 'Service' || usingAdminFallback) { if (restoring !== undefined) { setStep('applying') await patchVergeConfig(restoring) } - showNotice.success( - restoring === undefined - ? 'settings.sections.proxyControl.messages.installedCheckProxy' - : 'settings.sections.proxyControl.messages.installedProxyRestored', - ) + if (!usingAdminFallback) { + showNotice.success( + restoring === undefined + ? 'settings.sections.proxyControl.messages.installedCheckProxy' + : 'settings.sections.proxyControl.messages.installedProxyRestored', + ) + } close() } else { showNotice.error( diff --git a/src/locales/ar/settings.json b/src/locales/ar/settings.json index 4aec89f19..4fd6792cb 100644 --- a/src/locales/ar/settings.json +++ b/src/locales/ar/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "تم تثبيت الخدمة بنجاح", + "sidecarFallback": "تعذّر على الخدمة تشغيل النواة بسبب أذونات المسار. تم التبديل إلى وضع المسؤول لمواصلة التشغيل.", "uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح" }, "updater": { diff --git a/src/locales/de/settings.json b/src/locales/de/settings.json index 109f32725..e6d16ec5b 100644 --- a/src/locales/de/settings.json +++ b/src/locales/de/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Service erfolgreich installiert", + "sidecarFallback": "Der Dienst konnte den Kern aufgrund der Pfadberechtigungen nicht starten. Die Anwendung läuft im Administratormodus weiter.", "uninstallSuccess": "Service erfolgreich deinstalliert" }, "updater": { diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index fe0f9c94a..24450ae3f 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Service Installed Successfully", + "sidecarFallback": "The service could not start the core due to path permissions. Switched to administrator mode to continue running.", "uninstallSuccess": "Service Uninstalled Successfully" }, "updater": { diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index b8492bb2c..c686037e3 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Servicio instalado con éxito", + "sidecarFallback": "El servicio no pudo iniciar el núcleo debido a los permisos de la ruta. Se ha cambiado al modo administrador para continuar.", "uninstallSuccess": "Servicio desinstalado con éxito" }, "updater": { diff --git a/src/locales/fa/settings.json b/src/locales/fa/settings.json index f2b6fc10c..32cf0e447 100644 --- a/src/locales/fa/settings.json +++ b/src/locales/fa/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "سرویس با موفقیت نصب شد", + "sidecarFallback": "سرویس به‌دلیل مجوزهای مسیر نتوانست هسته را اجرا کند. برای ادامهٔ اجرا به حالت مدیر تغییر یافت.", "uninstallSuccess": "سرویس با موفقیت حذف نصب شد" }, "updater": { diff --git a/src/locales/id/settings.json b/src/locales/id/settings.json index cf7245cb7..aefc26171 100644 --- a/src/locales/id/settings.json +++ b/src/locales/id/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Layanan Berhasil Diinstal", + "sidecarFallback": "Layanan tidak dapat memulai inti karena izin jalur. Beralih ke mode administrator untuk melanjutkan.", "uninstallSuccess": "Layanan Berhasil Dicopot" }, "updater": { diff --git a/src/locales/jp/settings.json b/src/locales/jp/settings.json index a7ec639f7..b39537ecf 100644 --- a/src/locales/jp/settings.json +++ b/src/locales/jp/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "サービスのインストールに成功しました。", + "sidecarFallback": "パスの権限によりサービスがコアを起動できなかったため、管理者モードに切り替えて動作を継続しています。", "uninstallSuccess": "サービスのアンインストールに成功しました。" }, "updater": { diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 725607d0d..6aadcdc98 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "서비스가 설치되었습니다", + "sidecarFallback": "경로 권한 문제로 서비스가 코어를 시작하지 못해 관리자 모드로 전환하여 계속 실행합니다.", "uninstallSuccess": "서비스가 제거되었습니다" }, "updater": { diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index f96d6f6a5..cf5be05dd 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Служба успешно установлена", + "sidecarFallback": "Служба не смогла запустить ядро из-за прав доступа к пути. Работа продолжена в режиме администратора.", "uninstallSuccess": "Служба успешно удалена" }, "updater": { diff --git a/src/locales/tr/settings.json b/src/locales/tr/settings.json index 619b201ca..45dc3e4fc 100644 --- a/src/locales/tr/settings.json +++ b/src/locales/tr/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Hizmet Başarıyla Kuruldu", + "sidecarFallback": "Hizmet, yol izinleri nedeniyle çekirdeği başlatamadı. Çalışmaya devam etmek için yönetici moduna geçildi.", "uninstallSuccess": "Hizmet Başarıyla Kaldırıldı" }, "updater": { diff --git a/src/locales/tt/settings.json b/src/locales/tt/settings.json index 2aa3fb7ca..97c5e3abe 100644 --- a/src/locales/tt/settings.json +++ b/src/locales/tt/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "Сервис уңышлы урнаштырылды", + "sidecarFallback": "Хезмәт юлга керү хокуклары аркасында төшне эшләтеп җибәрә алмады. Эшне дәвам итү өчен администратор режимына күчелде.", "uninstallSuccess": "Сервис уңышлы салдырылды" }, "updater": { diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 42ac1101f..090e6fff0 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "已成功安装服务", + "sidecarFallback": "服务因路径权限问题无法启动内核,已切回管理员模式继续运行。", "uninstallSuccess": "已成功卸载服务" }, "updater": { diff --git a/src/locales/zhtw/settings.json b/src/locales/zhtw/settings.json index 6997082ac..89eb9689e 100644 --- a/src/locales/zhtw/settings.json +++ b/src/locales/zhtw/settings.json @@ -683,6 +683,7 @@ }, "clashService": { "installSuccess": "已成功安裝服務", + "sidecarFallback": "服務因路徑權限問題無法啟動核心,已切回管理員模式繼續執行。", "uninstallSuccess": "已成功解除安裝服務" }, "updater": { diff --git a/src/pages/_layout/hooks/use-layout-events.test.ts b/src/pages/_layout/hooks/use-layout-events.test.ts index 59edc8b34..0a3f50f4d 100644 --- a/src/pages/_layout/hooks/use-layout-events.test.ts +++ b/src/pages/_layout/hooks/use-layout-events.test.ts @@ -13,7 +13,10 @@ vi.mock('react', () => ({ useEffect: (effect: () => void) => effect() })) vi.mock('@/hooks/use-profiles', () => ({ revalidateProfiles: vi.fn() })) vi.mock('@/hooks/use-system-state', () => ({ runStateQueryKey: ['state'] })) vi.mock('@/services/events', () => ({ subscribeVergeEvents: vi.fn() })) -vi.mock('@/services/cmds', () => ({ takeDnsOverrideNotice: vi.fn() })) +vi.mock('@/services/cmds', () => ({ + takeDnsOverrideNotice: vi.fn(), + takeServiceFallbackNotice: vi.fn().mockResolvedValue(false), +})) vi.mock('@/services/notice-service', () => ({ showNotice: { info: vi.fn() }, })) diff --git a/src/pages/_layout/hooks/use-layout-events.ts b/src/pages/_layout/hooks/use-layout-events.ts index b1094cf0c..eddf2318f 100644 --- a/src/pages/_layout/hooks/use-layout-events.ts +++ b/src/pages/_layout/hooks/use-layout-events.ts @@ -64,6 +64,7 @@ export const useLayoutEvents = ( () => { revalidateKeys(['getRuntimeState', 'getVergeConfig']) handleNotice(['dns_override::auto_disabled', '']) + handleNotice(['service_core::sidecar_fallback', '']) }, ) }, [handleNotice]) diff --git a/src/pages/_layout/utils/notification-handlers.ts b/src/pages/_layout/utils/notification-handlers.ts index d866cd762..cff425e8e 100644 --- a/src/pages/_layout/utils/notification-handlers.ts +++ b/src/pages/_layout/utils/notification-handlers.ts @@ -1,4 +1,7 @@ -import { takeDnsOverrideNotice } from '@/services/cmds' +import { + takeDnsOverrideNotice, + takeServiceFallbackNotice, +} from '@/services/cmds' import { showNotice } from '@/services/notice-service' type NavigateFunction = (path: string, options?: any) => void @@ -25,6 +28,22 @@ export const handleNoticeMessage = ( showNotice.error(msg) }, 'set_config::error': () => showNotice.error(msg), + 'service_core::sidecar_fallback': () => { + void takeServiceFallbackNotice() + .then((pending) => { + if (pending) { + showNotice.warning( + 'settings.feedback.notifications.clashService.sidecarFallback', + ) + } + }) + .catch((error) => { + console.error( + 'Failed to read the pending service fallback notice', + error, + ) + }) + }, 'dns_override::auto_disabled': () => { void takeDnsOverrideNotice() .then((pending) => { diff --git a/src/services/cmds.ts b/src/services/cmds.ts index efea521b4..034d77efa 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -182,6 +182,10 @@ export async function takeDnsOverrideNotice() { return invoke('take_dns_override_notice') } +export async function takeServiceFallbackNotice() { + return invoke('take_service_fallback_notice') +} + export async function getSystemProxy() { return invoke<{ enable: boolean diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 928bdcd67..d2535331c 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -793,6 +793,7 @@ export const translationKeys = [ 'settings.feedback.notifications.clash.changeFailed', 'settings.feedback.notifications.clash.geoDataUpdated', 'settings.feedback.notifications.clashService.installSuccess', + 'settings.feedback.notifications.clashService.sidecarFallback', 'settings.feedback.notifications.clashService.uninstallSuccess', 'settings.feedback.notifications.updater.withClashProxySuccess', 'settings.feedback.notifications.updater.withClashProxyFailed', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index f92a33cd1..474314e5b 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -831,6 +831,7 @@ export interface TranslationResources { } clashService: { installSuccess: string + sidecarFallback: string uninstallSuccess: string } updater: {