From a889d0b1e53c2bdea0fcca7f1abde8804887131c Mon Sep 17 00:00:00 2001 From: Slinetrac Date: Sat, 25 Oct 2025 18:04:32 +0800 Subject: [PATCH] refactor(profile-switch): serialize switches with async queue and enrich frontend events --- src-tauri/src/cmd/profile.rs | 187 ++++++++++++++---- src-tauri/src/core/handle.rs | 3 +- src-tauri/src/core/notification.rs | 39 +++- src/pages/profiles.tsx | 294 ++++++++++++++++++----------- 4 files changed, 363 insertions(+), 160 deletions(-) diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index e3ea4439..5ef90aa7 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -23,9 +23,14 @@ use std::{ sync::atomic::{AtomicBool, AtomicU64, Ordering}, time::Duration, }; -use tokio::sync::Mutex; +use tokio::sync::{ + Mutex, + mpsc::{self, error::TrySendError}, +}; static SWITCH_MUTEX: OnceCell> = OnceCell::new(); +static SWITCH_QUEUE: OnceCell> = OnceCell::new(); +const SWITCH_QUEUE_CAPACITY: usize = 32; // 全局请求序列号跟踪,用于避免队列化执行 static CURRENT_REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); static CURRENT_SWITCHING_PROFILE: AtomicBool = AtomicBool::new(false); @@ -45,6 +50,113 @@ impl Drop for SwitchScope { } } +fn switch_queue_sender() -> &'static mpsc::Sender<(String, bool)> { + SWITCH_QUEUE.get_or_init(|| { + let (tx, mut rx) = mpsc::channel::<(String, bool)>(SWITCH_QUEUE_CAPACITY); + tokio::spawn(async move { + let mutex = SWITCH_MUTEX.get_or_init(|| Mutex::new(())); + while let Some((profile, notify)) = rx.recv().await { + let _guard = mutex.lock().await; + if let Err(err) = process_switch_task(profile.clone(), notify).await { + logging!( + error, + Type::Cmd, + "Failed to process profile switch task ({}): {}", + profile, + err + ); + } + } + }); + tx + }) +} + +async fn process_switch_task(profile_index: String, notify_success: bool) -> CmdResult<()> { + logging!( + info, + Type::Cmd, + "Processing queued profile switch: {} (notify={})", + profile_index, + notify_success + ); + + let switch_result = AssertUnwindSafe(patch_profiles_config_internal(IProfiles { + current: Some(profile_index.clone()), + items: None, + })) + .catch_unwind() + .await; + + let switch_result = match switch_result { + Ok(inner) => inner, + Err(_) => { + logging!( + error, + Type::Cmd, + "Panic occurred during profile switch: {}", + profile_index + ); + handle::Handle::notice_message( + "config_validate::panic", + format!("profile switch panic: {}", profile_index), + ); + handle::Handle::notify_profile_switch_finished( + profile_index.clone(), + false, + notify_success, + ); + return Ok(()); + } + }; + + let success = match switch_result { + Ok(val) => val, + Err(err) => { + logging!( + error, + Type::Cmd, + "Profile switch failed ({}): {}", + profile_index, + err + ); + handle::Handle::notice_message("config_validate::error", err.clone()); + handle::Handle::notify_profile_switch_finished( + profile_index.clone(), + false, + notify_success, + ); + return Ok(()); + } + }; + + handle::Handle::notify_profile_switch_finished(profile_index.clone(), success, notify_success); + + if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { + logging!( + warn, + Type::Cmd, + "Failed to close connections after profile switch ({}): {}", + profile_index, + err + ); + } + + if notify_success && success { + handle::Handle::notice_message("info", "Profile Switched"); + } + + logging!( + info, + Type::Cmd, + "Profile switch task finished: {} (success={})", + profile_index, + success + ); + + Ok(()) +} + #[tauri::command] pub async fn get_profiles() -> CmdResult { // 策略1: 尝试快速获取latest数据 @@ -593,51 +705,46 @@ pub async fn patch_profiles_config_by_profile_index(profile_index: String) -> Cm #[tauri::command] pub async fn switch_profile(profile_index: String, notify_success: bool) -> CmdResult { - logging!(info, Type::Cmd, "请求切换配置到: {}", &profile_index); - - let mutex = SWITCH_MUTEX.get_or_init(|| Mutex::new(())); - let _guard = mutex.lock().await; - - let switch_result = AssertUnwindSafe(patch_profiles_config_internal(IProfiles { - current: Some(profile_index.clone()), - items: None, - })) - .catch_unwind() - .await; - - let result = match switch_result { - Ok(inner) => inner?, - Err(_) => { + logging!( + info, + Type::Cmd, + "Queue profile switch to: {}", + &profile_index + ); + let sender = switch_queue_sender(); + match sender.try_send((profile_index.clone(), notify_success)) { + Ok(_) => Ok(true), + Err(TrySendError::Full(task)) => { + logging!( + warn, + Type::Cmd, + "Profile switch queue is full, waiting for space: {}", + &profile_index + ); + match sender.send(task).await { + Ok(_) => Ok(true), + Err(err) => { + logging!( + error, + Type::Cmd, + "Profile switch queue closed while waiting ({}): {}", + &profile_index, + err + ); + Err("switch profile queue unavailable".into()) + } + } + } + Err(TrySendError::Closed(_)) => { logging!( error, Type::Cmd, - "切换配置过程中发生panic,目标: {}", - profile_index + "Profile switch queue is closed, cannot enqueue: {}", + &profile_index ); - handle::Handle::notice_message( - "config_validate::panic", - format!("profile switch panic: {}", profile_index), - ); - handle::Handle::notify_profile_switch_finished(profile_index.clone(), false); - return Ok(false); + Err("switch profile queue unavailable".into()) } - }; - - if result { - handle::Handle::notify_profile_switch_finished(profile_index.clone(), true); - } else { - handle::Handle::notify_profile_switch_finished(profile_index.clone(), false); } - - if let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { - logging!(warn, Type::Cmd, "切换配置后关闭连接失败: {}", err); - } - - if notify_success && result { - handle::Handle::notice_message("info", "Profile Switched"); - } - - Ok(result) } /// 修改某个profile item的 diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 4d22c8a6..79991cbd 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -100,10 +100,11 @@ impl Handle { }); } - pub fn notify_profile_switch_finished(profile_id: String, success: bool) { + pub fn notify_profile_switch_finished(profile_id: String, success: bool, notify: bool) { Self::send_event(FrontendEvent::ProfileSwitchFinished { profile_id, success, + notify, }); } diff --git a/src-tauri/src/core/notification.rs b/src-tauri/src/core/notification.rs index aeef3592..d7c3627d 100644 --- a/src-tauri/src/core/notification.rs +++ b/src-tauri/src/core/notification.rs @@ -20,14 +20,34 @@ pub enum FrontendEvent { RefreshClash, RefreshVerge, RefreshProxy, - ProxiesUpdated { payload: serde_json::Value }, - NoticeMessage { status: String, message: String }, - ProfileChanged { current_profile_id: String }, - ProfileSwitchFinished { profile_id: String, success: bool }, - TimerUpdated { profile_index: String }, - ProfileUpdateStarted { uid: String }, - ProfileUpdateCompleted { uid: String }, - RustPanic { message: String, location: String }, + ProxiesUpdated { + payload: serde_json::Value, + }, + NoticeMessage { + status: String, + message: String, + }, + ProfileChanged { + current_profile_id: String, + }, + ProfileSwitchFinished { + profile_id: String, + success: bool, + notify: bool, + }, + TimerUpdated { + profile_index: String, + }, + ProfileUpdateStarted { + uid: String, + }, + ProfileUpdateCompleted { + uid: String, + }, + RustPanic { + message: String, + location: String, + }, } #[derive(Debug, Default)] @@ -172,9 +192,10 @@ impl NotificationSystem { FrontendEvent::ProfileSwitchFinished { profile_id, success, + notify, } => ( "profile-switch-finished", - Ok(json!({ "profileId": profile_id, "success": success })), + Ok(json!({ "profileId": profile_id, "success": success, "notify": notify })), ), FrontendEvent::TimerUpdated { profile_index } => { ("verge://timer-updated", Ok(json!(profile_index))) diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 61d90fab..fdb7cd08 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -61,7 +61,7 @@ import { import { showNotice } from "@/services/noticeService"; import { useSetLoadingCache, useThemeMode } from "@/services/states"; -// 记录profile切换状态 +// Record profile switch state const debugProfileSwitch = (action: string, profile: string, extra?: any) => { const timestamp = new Date().toISOString().substring(11, 23); console.log( @@ -73,6 +73,7 @@ const debugProfileSwitch = (action: string, profile: string, extra?: any) => { type ProfileSwitchFinishedPayload = { profileId: string; success: boolean; + notify: boolean; }; type RustPanicPayload = { @@ -80,6 +81,11 @@ type RustPanicPayload = { location: string; }; +type SwitchRequest = { + profileId: string; + notifySuccess: boolean; +}; + const normalizeProfileUrl = (value?: string) => { if (!value) return ""; const trimmed = value.trim(); @@ -140,7 +146,7 @@ const createImportLandingVerifier = ( if (currentCount > baselineCount) { console.log( - `[导入验证] 配置数量已增加: ${baselineCount} -> ${currentCount}`, + `[Import Verify] Configuration count increased: ${baselineCount} -> ${currentCount}`, ); return true; } @@ -158,7 +164,9 @@ const createImportLandingVerifier = ( } if (!hadBaselineProfile) { - console.log("[导入验证] 检测到新的订阅记录,判定为导入成功"); + console.log( + "[Import Verify] Detected new profile record; treating as success", + ); return true; } @@ -167,13 +175,15 @@ const createImportLandingVerifier = ( if (currentUpdated > baselineUpdated) { console.log( - `[导入验证] 订阅更新时间已更新 ${baselineUpdated} -> ${currentUpdated}`, + `[Import Verify] Profile timestamp updated ${baselineUpdated} -> ${currentUpdated}`, ); return true; } if (currentSignature !== baselineSignature) { - console.log("[导入验证] 订阅详情发生变化,判定为导入成功"); + console.log( + "[Import Verify] Profile details changed; treating as success", + ); return true; } @@ -256,28 +266,28 @@ const ProfilePage = () => { }; }, [addListener, mutateProfiles, t]); - // 添加紧急恢复功能 + // Add emergency recovery capability const onEmergencyRefresh = useLockFn(async () => { - console.log("[紧急刷新] 开始强制刷新所有数据"); + console.log("[Emergency Refresh] Starting forced refresh of all data"); try { - // 清除所有SWR缓存 + // Clear all SWR caches await mutate(() => true, undefined, { revalidate: false }); - // 强制重新获取配置数据 + // Force fetching profile data await mutateProfiles(undefined, { revalidate: true, rollbackOnError: false, }); - // 等待状态稳定后增强配置 + // Wait for state to stabilize before enhancing the profile await new Promise((resolve) => setTimeout(resolve, 500)); await onEnhance(false); - showNotice("success", "数据已强制刷新", 2000); + showNotice("success", "Data forcibly refreshed", 2000); } catch (error: any) { - console.error("[紧急刷新] 失败:", error); - showNotice("error", `紧急刷新失败: ${error.message}`, 4000); + console.error("[Emergency Refresh] Failed:", error); + showNotice("error", `Emergency refresh failed: ${error.message}`, 4000); } }); @@ -304,7 +314,7 @@ const ProfilePage = () => { const onImport = async () => { if (!url) return; - // 校验url是否为http/https + // Validate that the URL uses http/https if (!/^https?:\/\//i.test(url)) { showNotice("error", t("Invalid Profile URL")); return; @@ -334,7 +344,10 @@ const ProfilePage = () => { ); } } catch (verifyErr) { - console.warn("[导入验证] 获取配置状态失败:", verifyErr); + console.warn( + "[Import Verify] Failed to fetch profile state:", + verifyErr, + ); break; } } @@ -343,33 +356,33 @@ const ProfilePage = () => { }; try { - // 尝试正常导入 + // Attempt standard import await importProfile(url); await handleImportSuccess("Profile Imported Successfully"); return; } catch (initialErr) { - console.warn("[订阅导入] 首次导入失败:", initialErr); + console.warn("[Profile Import] Initial import failed:", initialErr); const alreadyImported = await waitForImportLanding(); if (alreadyImported) { console.warn( - "[订阅导入] 接口返回失败,但检测到订阅已导入,跳过回退导入流程", + "[Profile Import] API reported failure, but profile already imported; skipping rollback", ); await handleImportSuccess("Profile Imported Successfully"); return; } - // 首次导入失败且未检测到数据变更,尝试使用自身代理 + // Initial import failed without data change; try built-in proxy showNotice("info", t("Import failed, retrying with Clash proxy...")); try { - // 使用自身代理尝试导入 + // Attempt import using built-in proxy await importProfile(url, { with_proxy: false, self_proxy: true, }); await handleImportSuccess("Profile Imported with Clash proxy"); } catch (retryErr: any) { - // 回退导入也失败 + // Rollback import also failed const retryErrmsg = retryErr?.message || retryErr.toString(); showNotice( "error", @@ -384,7 +397,7 @@ const ProfilePage = () => { const currentProfileId = profiles.current ?? null; - // 强化的刷新策略 + // Enhanced refresh strategy const performRobustRefresh = async ( importVerifier: ImportLandingVerifier, ) => { @@ -395,43 +408,50 @@ const ProfilePage = () => { while (retryCount < maxRetries) { try { - console.log(`[导入刷新] 第${retryCount + 1}次尝试刷新配置数据`); + console.log( + `[Import Refresh] Attempt ${retryCount + 1} to refresh profile data`, + ); - // 强制刷新,绕过所有缓存 + // Force refresh and bypass caches await mutateProfiles(undefined, { revalidate: true, rollbackOnError: false, }); - // 等待状态稳定 + // Wait for state to stabilize await new Promise((resolve) => setTimeout(resolve, baseDelay * (retryCount + 1)), ); - // 验证刷新是否成功 + // Verify whether refresh succeeded const currentProfiles = await getProfiles(); const currentCount = currentProfiles?.items?.length || 0; if (currentCount > baselineCount) { console.log( - `[导入刷新] 配置刷新成功,配置数量 ${baselineCount} -> ${currentCount}`, + `[Import Refresh] Profile refresh succeeded; count ${baselineCount} -> ${currentCount}`, ); await onEnhance(false); return; } if (hasLanding(currentProfiles)) { - console.log("[导入刷新] 检测到订阅内容更新,判定刷新成功"); + console.log( + "[Import Refresh] Detected profile update; treating as success", + ); await onEnhance(false); return; } console.warn( - `[导入刷新] 配置数量未增加 (${currentCount}), 继续重试...`, + `[Import Refresh] Profile count unchanged (${currentCount}), retrying...`, ); retryCount++; } catch (error) { - console.error(`[导入刷新] 第${retryCount + 1}次刷新失败:`, error); + console.error( + `[Import Refresh] Attempt ${retryCount + 1} failed:`, + error, + ); retryCount++; await new Promise((resolve) => setTimeout(resolve, baseDelay * retryCount), @@ -439,10 +459,12 @@ const ProfilePage = () => { } } - // 所有重试失败后的最后尝试 - console.warn(`[导入刷新] 常规刷新失败,尝试清除缓存重新获取`); + // Final attempt after all retries fail + console.warn( + `[Import Refresh] Regular refresh failed; clearing cache and retrying`, + ); try { - // 清除SWR缓存并重新获取 + // Clear SWR cache and refetch await mutate("getProfiles", getProfiles(), { revalidate: true }); await onEnhance(false); showNotice( @@ -451,7 +473,10 @@ const ProfilePage = () => { 3000, ); } catch (finalError) { - console.error(`[导入刷新] 最终刷新尝试失败:`, finalError); + console.error( + `[Import Refresh] Final refresh attempt failed:`, + finalError, + ); showNotice( "error", t("Profile imported successfully, please restart if not visible"), @@ -471,25 +496,108 @@ const ProfilePage = () => { const [switchingProfileId, setSwitchingProfileId] = useState( null, ); - const pendingProfileRef = useRef(null); - const isSwitching = switchingProfileId != null; + const activeSwitchRef = useRef(null); + const pendingSwitchRef = useRef(null); + + const executeSwitch = useCallback( + async (targetProfile: string, notifySuccess: boolean) => { + const currentRequest = activeSwitchRef.current; + if (currentRequest?.profileId === targetProfile) { + if (currentRequest.notifySuccess !== notifySuccess) { + activeSwitchRef.current = { profileId: targetProfile, notifySuccess }; + } + return; + } + + activeSwitchRef.current = { profileId: targetProfile, notifySuccess }; + setSwitchingProfileId(targetProfile); + setActivatings((prev) => + prev.includes(targetProfile) ? prev : [...prev, targetProfile], + ); + + try { + const accepted = await switchProfileCommand( + targetProfile, + notifySuccess, + ); + if (!accepted) { + throw new Error(t("Profile switch failed")); + } + } catch (error: any) { + if (activeSwitchRef.current?.profileId === targetProfile) { + activeSwitchRef.current = null; + } + setSwitchingProfileId((prev) => (prev === targetProfile ? null : prev)); + setActivatings((prev) => prev.filter((id) => id !== targetProfile)); + showNotice( + "error", + error?.message || error?.toString?.() || String(error), + ); + await mutateProfiles(); + const next = pendingSwitchRef.current; + pendingSwitchRef.current = null; + if (next) { + executeSwitch(next.profileId, next.notifySuccess); + } + } + }, + [mutateProfiles, setActivatings, t], + ); + + const enqueueSwitch = useCallback( + (targetProfile: string, notifySuccess: boolean) => { + if (activeSwitchRef.current) { + pendingSwitchRef.current = { profileId: targetProfile, notifySuccess }; + return; + } + executeSwitch(targetProfile, notifySuccess); + }, + [executeSwitch], + ); useEffect(() => { let mounted = true; const unlistenPromise = listen( "profile-switch-finished", - (event) => { + async (event) => { if (!mounted) return; const payload = event.payload; if (!payload) return; - setActivatings((prev) => prev.filter((id) => id !== payload.profileId)); - setSwitchingProfileId((prev) => - prev === payload.profileId ? null : prev, - ); + const { profileId, success, notify } = payload; - if (!payload.success) { - showNotice("error", t("Profile switch failed")); + setActivatings((prev) => prev.filter((id) => id !== profileId)); + + const isActive = activeSwitchRef.current?.profileId === profileId; + if (isActive) { + activeSwitchRef.current = null; + setSwitchingProfileId((prev) => (prev === profileId ? null : prev)); + + if (success) { + await mutateLogs(); + await activateSelected(); + if (notify) { + showNotice("success", t("Profile Switched"), 1000); + } + } else { + showNotice("error", t("Profile switch failed")); + } + + await mutateProfiles(); + + const next = pendingSwitchRef.current; + pendingSwitchRef.current = null; + if (next && next.profileId !== profileId) { + executeSwitch(next.profileId, next.notifySuccess); + } + } else { + if (!success) { + showNotice("error", t("Profile switch failed")); + } + if (pendingSwitchRef.current?.profileId === profileId) { + pendingSwitchRef.current = null; + } + await mutateProfiles(); } }, ); @@ -498,73 +606,39 @@ const ProfilePage = () => { mounted = false; unlistenPromise.then((unlisten) => unlisten()).catch(() => {}); }; - }, [setActivatings, t]); - - const executeSwitch = useCallback( - async function run(targetProfile: string, notifySuccess: boolean) { - setSwitchingProfileId(targetProfile); - setActivatings((prev) => { - if (prev.includes(targetProfile)) return prev; - return [...prev, targetProfile]; - }); - - try { - const success = await switchProfileCommand( - targetProfile, - notifySuccess, - ); - if (!success) { - showNotice("error", t("Profile switch failed")); - return; - } - await mutateLogs(); - await activateSelected(); - if (notifySuccess) { - showNotice("success", t("Profile Switched"), 1000); - } - } catch (error: any) { - showNotice( - "error", - error?.message || error?.toString?.() || String(error), - ); - } finally { - setActivatings((prev) => prev.filter((id) => id !== targetProfile)); - setSwitchingProfileId(null); - await mutateProfiles(); - const next = pendingProfileRef.current; - pendingProfileRef.current = null; - if (next && next !== targetProfile) { - await run(next, true); - } - } - }, - [activateSelected, mutateLogs, mutateProfiles, setActivatings, t], - ); + }, [ + activateSelected, + enqueueSwitch, + executeSwitch, + mutateLogs, + mutateProfiles, + setActivatings, + t, + ]); const onSelect = useCallback( (targetProfile: string, force: boolean) => { - if (isSwitching) { - pendingProfileRef.current = targetProfile; + if (activeSwitchRef.current?.profileId === targetProfile) { return; } if (!force && targetProfile === currentProfileId) { debugProfileSwitch("ALREADY_CURRENT_IGNORED", targetProfile); return; } - executeSwitch(targetProfile, true); + enqueueSwitch(targetProfile, true); }, - [currentProfileId, executeSwitch, isSwitching], + [currentProfileId, enqueueSwitch], ); useEffect(() => { if (!current) return; - if (isSwitching) { - pendingProfileRef.current = current; + if (activeSwitchRef.current) { + pendingSwitchRef.current = { profileId: current, notifySuccess: false }; return; } if (current === currentProfileId) return; - executeSwitch(current, false); - }, [current, currentProfileId, executeSwitch, isSwitching]); + enqueueSwitch(current, false); + }, [current, currentProfileId, enqueueSwitch]); useEffect(() => { let mounted = true; @@ -587,7 +661,7 @@ const ProfilePage = () => { const onEnhance = useLockFn(async (notifySuccess: boolean) => { if (switchingProfileId) { console.log( - `[Profile] 有profile正在切换中(${switchingProfileId}),跳过enhance操作`, + `[Profile] A profile is currently switching (${switchingProfileId}); skipping enhance operation`, ); return; } @@ -604,7 +678,7 @@ const ProfilePage = () => { } catch (err: any) { showNotice("error", err.message || err.toString(), 3000); } finally { - // 保留正在切换的profile,清除其他状态 + // Keep the switching profile active and clear other states setActivatings((prev) => switchingProfileId ? prev.filter((id) => id === switchingProfileId) @@ -630,7 +704,7 @@ const ProfilePage = () => { } }); - // 更新所有订阅 + // Update all profiles const setLoadingCache = useSetLoadingCache(); const onUpdateAll = useLockFn(async () => { const throttleMutate = throttle(mutateProfiles, 2000, { @@ -641,7 +715,7 @@ const ProfilePage = () => { await updateProfile(uid); throttleMutate(); } catch (err: any) { - console.error(`更新订阅 ${uid} 失败:`, err); + console.error(`Failed to update profile ${uid}:`, err); } finally { setLoadingCache((cache) => ({ ...cache, [uid]: false })); } @@ -649,7 +723,7 @@ const ProfilePage = () => { return new Promise((resolve) => { setLoadingCache((cache) => { - // 获取没有正在更新的订阅 + // Gather profiles that are not updating const items = profileItems.filter( (e) => e.type === "remote" && !cache[e.uid], ); @@ -703,11 +777,11 @@ const ProfilePage = () => { const getSelectionState = () => { if (selectedProfiles.size === 0) { - return "none"; // 无选择 + return "none"; // no selection } else if (selectedProfiles.size === profileItems.length) { - return "all"; // 全选 + return "all"; // all selected } else { - return "partial"; // 部分选择 + return "partial"; // partially selected } }; @@ -754,7 +828,7 @@ const ProfilePage = () => { ? "rgba(0, 0, 0, 0.06)" : "rgba(255, 255, 255, 0.06)"; - // 监听后端配置变更 + // Observe configuration changes from backend useEffect(() => { let unlistenPromise: Promise<() => void> | undefined; let lastProfileId: string | null = null; @@ -768,29 +842,29 @@ const ProfilePage = () => { const newProfileId = event.payload; const now = Date.now(); - console.log(`[Profile] 收到配置变更事件: ${newProfileId}`); + console.log(`[Profile] Received profile-change event: ${newProfileId}`); if ( lastProfileId === newProfileId && now - lastUpdateTime < debounceDelay ) { - console.log(`[Profile] 重复事件被防抖,跳过`); + console.log(`[Profile] Duplicate event throttled; skipping`); return; } lastProfileId = newProfileId; lastUpdateTime = now; - console.log(`[Profile] 执行配置数据刷新`); + console.log(`[Profile] Performing profile data refresh`); if (refreshTimer !== null) { window.clearTimeout(refreshTimer); } - // 使用异步调度避免阻塞事件处理 + // Use async scheduling to avoid blocking event handling refreshTimer = window.setTimeout(() => { mutateProfiles().catch((error) => { - console.error("[Profile] 配置数据刷新失败:", error); + console.error("[Profile] Profile data refresh failed:", error); }); refreshTimer = null; }, 0); @@ -853,12 +927,12 @@ const ProfilePage = () => { - {/* 故障检测和紧急恢复按钮 */} + {/* Fault detection and emergency recovery button */} {(error || isStale) && ( { ref={viewerRef} onChange={async (isActivating) => { mutateProfiles(); - // 只有更改当前激活的配置时才触发全局重新加载 + // Only trigger global reload when the active profile changes if (isActivating) { await onEnhance(false); }