From 5a4c3e9eb8282e826733be20a9239bf5e9b4fb78 Mon Sep 17 00:00:00 2001 From: moranfanhua Date: Sat, 24 Oct 2026 00:48:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=AF=8F?= =?UTF-8?q?=E6=97=A5=E5=AE=9A=E6=97=B6=E6=A3=80=E6=9F=A5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在更新设置中增加定时检查开关,默认关闭,时间默认为 08:00 - 将开关和时间独立保存到应用设置,修改后立即重排每日检查 - 复用更新结果处理,保持原有启动检查及自动任务挂起行为 - 仅为定时入口增加并发与参数变化校验,并同步五种语言文案 关联 MaaXYZ/MXU#358 验证:构建、类型及格式检查通过,启动逻辑通过与原代码的 16 种模拟场景对照。 --- src/App.tsx | 132 ++++++++++++++++++++-- src/components/settings/UpdateSection.tsx | 33 ++++++ src/i18n/locales/en-US.ts | 2 + src/i18n/locales/ja-JP.ts | 2 + src/i18n/locales/ko-KR.ts | 2 + src/i18n/locales/zh-CN.ts | 2 + src/i18n/locales/zh-TW.ts | 2 + src/stores/appStore.ts | 17 +++ src/stores/types.ts | 6 + src/types/config.ts | 12 ++ 10 files changed, 199 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8bd03438..86addfe1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { flushConfig, flushSaveConfig, type DownloadProgress, + type UpdateInfo, } from '@/stores/appStore'; import { TitleBar, @@ -208,6 +209,8 @@ function App() { onboardingCompleted, backgroundImage, backgroundOpacity, + scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime, } = useAppStore( useShallow((state) => ({ setProjectInterface: state.setProjectInterface, @@ -250,6 +253,8 @@ function App() { onboardingCompleted: state.onboardingCompleted, backgroundImage: state.backgroundImage, backgroundOpacity: state.backgroundOpacity, + scheduledUpdateCheckEnabled: state.scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime: state.scheduledUpdateCheckTime, })), ); @@ -406,6 +411,7 @@ function App() { }, []); const initialized = useRef(false); + const scheduledUpdateCheckInProgressRef = useRef(false); const downloadStartedRef = useRef(false); const pendingAutoTasksRef = useRef(false); // 自动任务已分发但尚未完成启动时,也要阻止下载完成触发自动安装。 @@ -509,6 +515,120 @@ function App() { [setDownloadStatus, setDownloadProgress, setDownloadSavePath, tryAutoInstallUpdate], ); + // 启动检查和定时检查共用更新结果处理 + const handleAutoUpdateCheckResult = useCallback( + (updateResult: UpdateInfo) => { + setUpdateInfo(updateResult); + if (updateResult.hasUpdate) { + log.info(`发现新版本: ${updateResult.versionName}`); + useAppStore.getState().setShowUpdateDialog(true); + if (updateResult.downloadUrl) { + startAutoDownload(updateResult); + } + } else if (updateResult.errorCode) { + log.warn(`更新检查返回错误: code=${updateResult.errorCode}`); + useAppStore.getState().setShowUpdateDialog(true); + } + }, + [setUpdateInfo, startAutoDownload], + ); + + // 仅桌面端负责定时更新,避免多个 WebUI 页面重复触发 + useEffect(() => { + if ( + !scheduledUpdateCheckEnabled || + loadingState !== 'success' || + !isTauri() || + import.meta.env.DEV + ) { + return; + } + if ( + !projectInterface?.mirrorchyan_rid || + !projectInterface.version || + isDebugVersion(projectInterface.version) + ) { + return; + } + + const { mirrorchyan_rid: resourceId, version: currentVersion, github, name } = projectInterface; + let cancelled = false; + const isUpdateBusy = () => { + const state = useAppStore.getState(); + return ( + state.updateCheckLoading || + state.downloadStatus === 'downloading' || + state.downloadStatus === 'completed' || + state.installStatus !== 'idle' + ); + }; + + const checkForScheduledUpdate = async () => { + if (scheduledUpdateCheckInProgressRef.current || isUpdateBusy()) return; + scheduledUpdateCheckInProgressRef.current = true; + const { mirrorChyanSettings, proxySettings } = useAppStore.getState(); + try { + log.info('定时检查更新'); + const updateResult = await checkAndPrepareDownload({ + resourceId, + currentVersion, + cdk: mirrorChyanSettings.cdk || undefined, + channel: mirrorChyanSettings.channel, + userAgent: 'MXU', + githubUrl: github, + githubPat: mirrorChyanSettings.githubPat || undefined, + proxyUrl: proxySettings?.url, + projectName: name, + }); + const current = useAppStore.getState(); + // 只在检查参数或更新状态变化时丢弃结果,不比较配置对象引用 + if ( + cancelled || + isUpdateBusy() || + current.mirrorChyanSettings.cdk !== mirrorChyanSettings.cdk || + current.mirrorChyanSettings.channel !== mirrorChyanSettings.channel || + current.mirrorChyanSettings.githubPat !== mirrorChyanSettings.githubPat || + current.proxySettings?.url !== proxySettings?.url + ) { + return; + } + if (updateResult) handleAutoUpdateCheckResult(updateResult); + } catch (err) { + log.warn('定时检查更新失败:', err); + } finally { + scheduledUpdateCheckInProgressRef.current = false; + } + }; + + const [hour, minute] = scheduledUpdateCheckTime.split(':').map(Number); + let timer: ReturnType; + const scheduleNextCheck = () => { + const now = new Date(); + const nextCheck = new Date(now); + nextCheck.setHours(hour, minute, 0, 0); + if (nextCheck.getTime() <= now.getTime()) { + nextCheck.setDate(nextCheck.getDate() + 1); + } + + timer = setTimeout(() => { + void checkForScheduledUpdate(); + scheduleNextCheck(); + }, nextCheck.getTime() - now.getTime()); + }; + + scheduleNextCheck(); + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [ + loadingState, + projectInterface, + scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime, + handleAutoUpdateCheckResult, + ]); + // 设置窗口标题(根据 ProjectInterface V2 协议) useEffect(() => { if (!projectInterface) return; @@ -1075,17 +1195,7 @@ function App() { projectName: result.interface.name, }); if (updateResult) { - setUpdateInfo(updateResult); - if (updateResult.hasUpdate) { - log.info(`发现新版本: ${updateResult.versionName}`); - useAppStore.getState().setShowUpdateDialog(true); - if (updateResult.downloadUrl) { - startAutoDownload(updateResult); - } - } else if (updateResult.errorCode) { - log.warn(`更新检查返回错误: code=${updateResult.errorCode}`); - useAppStore.getState().setShowUpdateDialog(true); - } + handleAutoUpdateCheckResult(updateResult); } } catch (err) { log.warn('自动检查更新失败:', err); diff --git a/src/components/settings/UpdateSection.tsx b/src/components/settings/UpdateSection.tsx index bd9aaf3e..e6503896 100644 --- a/src/components/settings/UpdateSection.tsx +++ b/src/components/settings/UpdateSection.tsx @@ -13,6 +13,7 @@ import { PackageCheck, Bug, Network, + Clock, } from 'lucide-react'; import clsx from 'clsx'; @@ -30,6 +31,7 @@ import { createProxySettings, proxySettingsForUpdateDownload } from '@/services/ import { resolveI18nText } from '@/services/contentResolver'; import { getInterfaceLangKey } from '@/i18n'; import { loggers } from '@/utils/logger'; +import { SwitchButton, TimeInput } from '../FormControls'; import { ReleaseNotes, DownloadProgressBar } from '../UpdateInfoCard'; export function UpdateSection() { @@ -42,6 +44,10 @@ export function UpdateSection() { mirrorChyanSettings, setMirrorChyanCdk, setMirrorChyanChannel, + scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime, + setScheduledUpdateCheckEnabled, + setScheduledUpdateCheckTime, proxySettings, setProxySettings, updateInfo, @@ -415,6 +421,33 @@ export function UpdateSection() { + {/* 定时检查更新 */} +
+
+
+ + + {t('settings.scheduledUpdateCheck')} + +
+ +
+ {scheduledUpdateCheckEnabled && ( + + )} +
+ {/* CDK 输入 */}
diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index dd052f03..3bec2f56 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -50,6 +50,8 @@ export default { appearance: 'Appearance', hotkeys: 'Hotkeys', general: 'General', + scheduledUpdateCheck: 'Scheduled Update Checks', + scheduledUpdateCheckTime: 'Daily Check Time (Local Time)', taskSettings: 'Task Settings', taskSettingsEmpty: 'No settings available to display', language: 'Language', diff --git a/src/i18n/locales/ja-JP.ts b/src/i18n/locales/ja-JP.ts index 52e6a68e..1a7d8aa9 100644 --- a/src/i18n/locales/ja-JP.ts +++ b/src/i18n/locales/ja-JP.ts @@ -49,6 +49,8 @@ export default { appearance: '外観', hotkeys: 'ショートカットキー', general: '一般', + scheduledUpdateCheck: '更新の定時確認', + scheduledUpdateCheckTime: '毎日の確認時刻(現地時間)', taskSettings: 'タスク設定', taskSettingsEmpty: '表示できる設定項目がありません', language: '言語', diff --git a/src/i18n/locales/ko-KR.ts b/src/i18n/locales/ko-KR.ts index 04917402..f43170b1 100644 --- a/src/i18n/locales/ko-KR.ts +++ b/src/i18n/locales/ko-KR.ts @@ -49,6 +49,8 @@ export default { appearance: '외관', hotkeys: '단축키', general: '일반', + scheduledUpdateCheck: '예약 업데이트 확인', + scheduledUpdateCheckTime: '매일 확인 시간 (현지 시간)', taskSettings: '작업 설정', taskSettingsEmpty: '표시할 수 있는 설정 항목이 없습니다', language: '언어', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index cdbca16a..a844e6be 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -50,6 +50,8 @@ export default { appearance: '外观', hotkeys: '快捷键', general: '通用', + scheduledUpdateCheck: '定时检查更新', + scheduledUpdateCheckTime: '每日检查时间(本地时间)', taskSettings: '任务设置', taskSettingsEmpty: '当前没有可显示的设置项', language: '语言', diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index fa4098b9..d79420bb 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -49,6 +49,8 @@ export default { appearance: '外觀', hotkeys: '快捷鍵', general: '一般', + scheduledUpdateCheck: '定時檢查更新', + scheduledUpdateCheckTime: '每日檢查時間(本地時間)', taskSettings: '任務設定', taskSettingsEmpty: '目前沒有可顯示的設定項', language: '語言', diff --git a/src/stores/appStore.ts b/src/stores/appStore.ts index 7966539d..5a46d9cd 100644 --- a/src/stores/appStore.ts +++ b/src/stores/appStore.ts @@ -22,9 +22,11 @@ import { DEFAULT_MAX_LOGS_PER_INSTANCE, defaultAddTaskPanelHeight, defaultMirrorChyanSettings, + defaultScheduledUpdateCheckTime, defaultScreenshotFrameRate, defaultWindowSize, normalizeAddTaskPanelHeight, + normalizeScheduledUpdateCheckTime, } from '@/types/config'; import type { ActionConfig, @@ -1493,6 +1495,10 @@ export const useAppStore = create()( confirmBeforeDelete: config.settings.confirmBeforeDelete ?? false, maxLogsPerInstance: config.settings.maxLogsPerInstance ?? DEFAULT_MAX_LOGS_PER_INSTANCE, autoClearLogsOnLaunch: config.settings.autoClearLogsOnLaunch ?? false, + scheduledUpdateCheckEnabled: config.settings.scheduledUpdateCheckEnabled === true, + scheduledUpdateCheckTime: normalizeScheduledUpdateCheckTime( + config.settings.scheduledUpdateCheckTime, + ), // 默认开启;调试 / 开发版本强制关闭 helpImproveSoftware: isTelemetryBlockedByBuild(get().projectInterface) ? false @@ -2013,6 +2019,13 @@ export const useAppStore = create()( mirrorChyanSettings: { ...state.mirrorChyanSettings, channel }, })), + // 定时检查更新 + scheduledUpdateCheckEnabled: false, + scheduledUpdateCheckTime: defaultScheduledUpdateCheckTime, + setScheduledUpdateCheckEnabled: (enabled) => set({ scheduledUpdateCheckEnabled: enabled }), + setScheduledUpdateCheckTime: (time) => + set({ scheduledUpdateCheckTime: normalizeScheduledUpdateCheckTime(time) }), + // 代理设置 proxySettings: undefined, setProxySettings: (settings) => set({ proxySettings: settings }), @@ -2482,6 +2495,8 @@ function generateConfig(): MxuConfig { confirmBeforeDelete: state.confirmBeforeDelete, maxLogsPerInstance: state.maxLogsPerInstance, autoClearLogsOnLaunch: state.autoClearLogsOnLaunch, + scheduledUpdateCheckEnabled: state.scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime: state.scheduledUpdateCheckTime, helpImproveSoftware: state.helpImproveSoftware, windowSize: bl?.windowSize ?? state.windowSize, windowPosition: bl?.windowPosition ?? state.windowPosition, @@ -2577,6 +2592,8 @@ useAppStore.subscribe( autoClearLogsOnLaunch: state.autoClearLogsOnLaunch, helpImproveSoftware: state.helpImproveSoftware, mirrorChyanSettings: state.mirrorChyanSettings, + scheduledUpdateCheckEnabled: state.scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime: state.scheduledUpdateCheckTime, proxySettings: state.proxySettings, welcomeShownHash: state.welcomeShownHash, devMode: state.devMode, diff --git a/src/stores/types.ts b/src/stores/types.ts index 6ba8cf33..dfc4ed73 100644 --- a/src/stores/types.ts +++ b/src/stores/types.ts @@ -374,6 +374,12 @@ export interface AppState { setMirrorChyanCdk: (cdk: string) => void; setMirrorChyanChannel: (channel: UpdateChannel) => void; + // 定时检查更新 + scheduledUpdateCheckEnabled: boolean; + scheduledUpdateCheckTime: string; + setScheduledUpdateCheckEnabled: (enabled: boolean) => void; + setScheduledUpdateCheckTime: (time: string) => void; + // 代理设置 proxySettings: ProxySettings | undefined; setProxySettings: (settings: ProxySettings | undefined) => void; diff --git a/src/types/config.ts b/src/types/config.ts index 0931274b..71211a6f 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -144,6 +144,8 @@ export interface AppSettings { /** 每个实例最多保留的日志条数(超出自动丢弃最旧的) */ maxLogsPerInstance?: number; autoClearLogsOnLaunch?: boolean; + scheduledUpdateCheckEnabled?: boolean; // 是否每天定时检查更新,默认关闭 + scheduledUpdateCheckTime?: string; // 每天检查更新的本地时间(HH:mm),默认 08:00 windowSize?: WindowSize; windowPosition?: WindowPosition; // 窗口位置 mirrorChyan?: MirrorChyanSettings; @@ -198,6 +200,14 @@ export const defaultWindowSize: WindowSize = { height: 618, }; +export const defaultScheduledUpdateCheckTime = '08:00'; + +export function normalizeScheduledUpdateCheckTime(value: unknown): string { + return typeof value === 'string' && /^([01]\d|2[0-3]):[0-5]\d$/.test(value) + ? value + : defaultScheduledUpdateCheckTime; +} + // 默认 MirrorChyan 设置 export const defaultMirrorChyanSettings: MirrorChyanSettings = { cdk: '', @@ -244,6 +254,8 @@ export const defaultConfig: MxuConfig = { confirmBeforeDelete: false, maxLogsPerInstance: DEFAULT_MAX_LOGS_PER_INSTANCE, autoClearLogsOnLaunch: false, + scheduledUpdateCheckEnabled: false, + scheduledUpdateCheckTime: defaultScheduledUpdateCheckTime, windowSize: defaultWindowSize, mirrorChyan: defaultMirrorChyanSettings, helpImproveSoftware: true, From d4e924c8a9d31c26af68080fbf34cc0a4aec8f5a Mon Sep 17 00:00:00 2001 From: moranfanhua Date: Sat, 24 Oct 2026 01:04:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A3=80=E6=9F=A5=E7=AB=9E=E4=BA=89=E5=8F=8A?= =?UTF-8?q?=E7=A9=BA=E5=AE=9E=E4=BE=8B=E9=85=8D=E7=BD=AE=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 25 ++++++++++++++++------- src/components/settings/UpdateSection.tsx | 3 ++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 86addfe1..05c01e1b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -90,7 +90,12 @@ import { WebUIBetaBanner } from './components/app/WebUIBetaBanner'; import { startGlobalCallbackListener } from './components/connection/callbackCache'; import { useIsMobile } from '@/hooks/useIsMobile'; import { ScrollText } from 'lucide-react'; -import { defaultConfig, defaultWindowSize, isValidMxuConfig } from '@/types/config'; +import { + defaultConfig, + defaultScheduledUpdateCheckTime, + defaultWindowSize, + isValidMxuConfig, +} from '@/types/config'; const log = loggers.app; @@ -411,7 +416,6 @@ function App() { }, []); const initialized = useRef(false); - const scheduledUpdateCheckInProgressRef = useRef(false); const downloadStartedRef = useRef(false); const pendingAutoTasksRef = useRef(false); // 自动任务已分发但尚未完成启动时,也要阻止下载完成触发自动安装。 @@ -556,7 +560,6 @@ function App() { const isUpdateBusy = () => { const state = useAppStore.getState(); return ( - state.updateCheckLoading || state.downloadStatus === 'downloading' || state.downloadStatus === 'completed' || state.installStatus !== 'idle' @@ -564,9 +567,10 @@ function App() { }; const checkForScheduledUpdate = async () => { - if (scheduledUpdateCheckInProgressRef.current || isUpdateBusy()) return; - scheduledUpdateCheckInProgressRef.current = true; - const { mirrorChyanSettings, proxySettings } = useAppStore.getState(); + const state = useAppStore.getState(); + if (state.updateCheckLoading || isUpdateBusy()) return; + state.setUpdateCheckLoading(true); + const { mirrorChyanSettings, proxySettings } = state; try { log.info('定时检查更新'); const updateResult = await checkAndPrepareDownload({ @@ -596,7 +600,7 @@ function App() { } catch (err) { log.warn('定时检查更新失败:', err); } finally { - scheduledUpdateCheckInProgressRef.current = false; + state.setUpdateCheckLoading(false); } }; @@ -768,6 +772,13 @@ function App() { // 应用配置 if (config.instances.length > 0) { importConfig(config); + } else { + // 无实例时单独恢复定时检查偏好 + const state = useAppStore.getState(); + state.setScheduledUpdateCheckEnabled(config.settings.scheduledUpdateCheckEnabled === true); + state.setScheduledUpdateCheckTime( + config.settings.scheduledUpdateCheckTime ?? defaultScheduledUpdateCheckTime, + ); } // 初始化匿名遥测(仅当 interface 声明了 telemetry.sentry.dsn 且非调试 / 开发版本) diff --git a/src/components/settings/UpdateSection.tsx b/src/components/settings/UpdateSection.tsx index e6503896..6fbde039 100644 --- a/src/components/settings/UpdateSection.tsx +++ b/src/components/settings/UpdateSection.tsx @@ -206,7 +206,7 @@ export function UpdateSection() { isPendingGitHubDownload || noUpdateInfoYet); - if (shouldTryMirrorChyan) { + if (shouldTryMirrorChyan && !useAppStore.getState().updateCheckLoading) { if (isDownloadingFromGitHub) { addDebugLog('检测到填入 CDK,正在停止 GitHub 下载并切换到 Mirror酱...'); await cancelDownload(); @@ -315,6 +315,7 @@ export function UpdateSection() { // 检查更新 const handleCheckUpdate = async () => { + if (useAppStore.getState().updateCheckLoading) return; if (!projectInterface?.mirrorchyan_rid || !projectInterface?.version) { addDebugLog('未配置 mirrorchyan_rid 或 version,无法检查更新'); return;