diff --git a/OpenBitFun-Installer/AGENTS-CN.md b/OpenBitFun-Installer/AGENTS-CN.md index e096c75d23..6be918588c 100644 --- a/OpenBitFun-Installer/AGENTS-CN.md +++ b/OpenBitFun-Installer/AGENTS-CN.md @@ -26,6 +26,8 @@ Language Select → Options → Progress → Model Setup → Theme Setup ``` +如果所选路径已有 OpenBitFun 安装,流程会在 `Progress` 后结束,保留已有的模型和主题配置。 + ## 命令 这些是命令参考,不是默认预检清单。PR 范围请以下方 Verification 为准。 diff --git a/OpenBitFun-Installer/AGENTS.md b/OpenBitFun-Installer/AGENTS.md index 336bcd60b2..10f1e5bff5 100644 --- a/OpenBitFun-Installer/AGENTS.md +++ b/OpenBitFun-Installer/AGENTS.md @@ -26,6 +26,9 @@ Install flow: Language Select → Options → Progress → Model Setup → Theme Setup ``` +For an existing installation at the selected path, the flow ends after +`Progress`; model and theme setup are retained from the existing installation. + ## Commands These are command references, not the default precheck list. Use Verification diff --git a/OpenBitFun-Installer/README.md b/OpenBitFun-Installer/README.md index bed1129cf5..e33607a651 100644 --- a/OpenBitFun-Installer/README.md +++ b/OpenBitFun-Installer/README.md @@ -114,6 +114,11 @@ Language Select → Options → Progress → Model Setup → Theme Setup language options install model config launch/close ``` +When the selected installation path already contains the current OpenBitFun +installation, the update flow ends after Progress. The existing model and theme +configuration are preserved, and the installer launches the updated application +when launch-after-install is enabled. + ## Development ### Prerequisites diff --git a/OpenBitFun-Installer/src/hooks/useInstaller.ts b/OpenBitFun-Installer/src/hooks/useInstaller.ts index aa52b6a45f..ab50d6b1db 100644 --- a/OpenBitFun-Installer/src/hooks/useInstaller.ts +++ b/OpenBitFun-Installer/src/hooks/useInstaller.ts @@ -52,6 +52,23 @@ export interface UseInstallerReturn { const STEPS: InstallStep[] = ['lang', 'options', 'progress', 'model', 'theme']; +function normalizeInstallPath(path: string): string { + return path.trim().replace(/[\\/]+$/, ''); +} + +function isSameInstallPath(left: string, right: string): boolean { + const normalizedLeft = normalizeInstallPath(left); + const normalizedRight = normalizeInstallPath(right); + if (normalizedLeft === normalizedRight) return true; + + // Windows registry paths are case-insensitive. Keep POSIX paths case-sensitive. + const isWindowsPath = /^[A-Za-z]:[\\/]/.test(normalizedLeft) + || /^[A-Za-z]:[\\/]/.test(normalizedRight) + || normalizedLeft.includes('\\') + || normalizedRight.includes('\\'); + return isWindowsPath && normalizedLeft.toLowerCase() === normalizedRight.toLowerCase(); +} + export function useInstaller(): UseInstallerReturn { const [previewOnly, setPreviewOnly] = useState(false); const [step, setStep] = useState('lang'); @@ -243,9 +260,16 @@ export function useInstaller(): UseInstallerReturn { setInstallationCompleted(false); setCanConfirmProgress(false); try { + const detectedInstall = await refreshExistingInstall(); const validated = await invoke('validate_install_path', { path: options.installPath, }); + const isUpdate = Boolean( + detectedInstall.detected + && detectedInstall.mainBinaryPresent + && detectedInstall.installLocation + && isSameInstallPath(detectedInstall.installLocation, validated.installPath), + ); const effectiveOptions = { ...options, installPath: validated.installPath, @@ -257,6 +281,13 @@ export function useInstaller(): UseInstallerReturn { setProgress({ step: 'prepare', percent: 0, message: '' }); await invoke('start_installation', { options: effectiveOptions }); setInstallationCompleted(true); + if (isUpdate) { + if (options.launchAfterInstall) { + await invoke('launch_application', { installPath: effectiveOptions.installPath }); + } + await invoke('close_installer'); + return; + } setStep('model'); try { const info = await readExistingInstall(); @@ -270,7 +301,7 @@ export function useInstaller(): UseInstallerReturn { } finally { setIsInstalling(false); } - }, [options, readExistingInstall]); + }, [options, readExistingInstall, refreshExistingInstall]); const confirmProgress = useCallback(() => { if (!canConfirmProgress) return;