diff --git a/src/App.tsx b/src/App.tsx index 8bd03438..05c01e1b 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, @@ -89,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; @@ -208,6 +214,8 @@ function App() { onboardingCompleted, backgroundImage, backgroundOpacity, + scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime, } = useAppStore( useShallow((state) => ({ setProjectInterface: state.setProjectInterface, @@ -250,6 +258,8 @@ function App() { onboardingCompleted: state.onboardingCompleted, backgroundImage: state.backgroundImage, backgroundOpacity: state.backgroundOpacity, + scheduledUpdateCheckEnabled: state.scheduledUpdateCheckEnabled, + scheduledUpdateCheckTime: state.scheduledUpdateCheckTime, })), ); @@ -509,6 +519,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.downloadStatus === 'downloading' || + state.downloadStatus === 'completed' || + state.installStatus !== 'idle' + ); + }; + + const checkForScheduledUpdate = async () => { + const state = useAppStore.getState(); + if (state.updateCheckLoading || isUpdateBusy()) return; + state.setUpdateCheckLoading(true); + const { mirrorChyanSettings, proxySettings } = state; + 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 { + state.setUpdateCheckLoading(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; @@ -648,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 且非调试 / 开发版本) @@ -1075,17 +1206,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..6fbde039 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, @@ -200,7 +206,7 @@ export function UpdateSection() { isPendingGitHubDownload || noUpdateInfoYet); - if (shouldTryMirrorChyan) { + if (shouldTryMirrorChyan && !useAppStore.getState().updateCheckLoading) { if (isDownloadingFromGitHub) { addDebugLog('检测到填入 CDK,正在停止 GitHub 下载并切换到 Mirror酱...'); await cancelDownload(); @@ -309,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; @@ -415,6 +422,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,