From 2f475dddc0bd8ee991fe09e369f9e8e356c591ac Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 27 Feb 2026 10:45:57 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(windows-upgrade):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DWindows=E5=8D=87=E7=BA=A7=E5=90=8E=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=88=97=E8=A1=A8=E4=B8=A2=E5=A4=B1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启动参数新增固定 WebviewUserDataPath 到 %APPDATA%/GoNavi/WebView2 - 首次启动自动迁移历史 WebView 数据目录 - 保留现有存储键,避免破坏已落盘配置 - 前端持久化读取增加历史结构兼容 - refs #125 --- frontend/src/store.ts | 39 ++++++-- main.go | 1 + main_windows_webview_userdata.go | 123 ++++++++++++++++++++++++++ main_windows_webview_userdata_stub.go | 7 ++ 4 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 main_windows_webview_userdata.go create mode 100644 main_windows_webview_userdata_stub.go diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 51281d4..5dd95a1 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -206,10 +206,27 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => { return safeConfig; }; +const resolveConnectionConfigPayload = (raw: Record): unknown => { + if (raw.config && typeof raw.config === 'object') { + return raw.config; + } + // 兼容历史/导入场景:连接对象可能是扁平结构(无 config 包装)。 + const hasLegacyFlatConfig = + raw.type !== undefined || + raw.host !== undefined || + raw.port !== undefined || + raw.user !== undefined || + raw.database !== undefined; + if (hasLegacyFlatConfig) { + return raw; + } + return undefined; +}; + const sanitizeSavedConnection = (value: unknown, index: number): SavedConnection | null => { if (!value || typeof value !== 'object') return null; const raw = value as Record; - const config = sanitizeConnectionConfig(raw.config); + const config = sanitizeConnectionConfig(resolveConnectionConfigPayload(raw)); const id = toTrimmedString(raw.id, `conn-${index + 1}`) || `conn-${index + 1}`; const fallbackName = config.host ? `${config.type}-${config.host}` : `连接-${index + 1}`; const name = toTrimmedString(raw.name, fallbackName) || fallbackName; @@ -392,6 +409,17 @@ const sanitizeAppearance = ( return nextAppearance; }; +const unwrapPersistedAppState = (persistedState: unknown): Record => { + if (!persistedState || typeof persistedState !== 'object') { + return {}; + } + const raw = persistedState as Record; + if (raw.state && typeof raw.state === 'object') { + return raw.state as Record; + } + return raw; +}; + export const useStore = create()( persist( (set) => ({ @@ -544,10 +572,7 @@ export const useStore = create()( name: 'lite-db-storage', // name of the item in the storage (must be unique) version: 3, migrate: (persistedState: unknown, version: number) => { - if (!persistedState || typeof persistedState !== 'object') { - return persistedState as AppState; - } - const state = persistedState as Partial; + const state = unwrapPersistedAppState(persistedState) as Partial; const nextState: Partial = { ...state }; nextState.connections = sanitizeConnections(state.connections); nextState.savedQueries = sanitizeSavedQueries(state.savedQueries); @@ -560,9 +585,7 @@ export const useStore = create()( return nextState as AppState; }, merge: (persistedState, currentState) => { - const state = (persistedState && typeof persistedState === 'object') - ? persistedState as Partial - : {}; + const state = unwrapPersistedAppState(persistedState) as Partial; return { ...currentState, ...state, diff --git a/main.go b/main.go index 44f3cd1..02cedcb 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ func main() { BackdropType: windows.Acrylic, DisableWindowIcon: false, DisableFramelessWindowDecorations: false, + WebviewUserDataPath: resolveWindowsWebviewUserDataPath(), }, Mac: &mac.Options{ WebviewIsTransparent: true, diff --git a/main_windows_webview_userdata.go b/main_windows_webview_userdata.go new file mode 100644 index 0000000..dcf2748 --- /dev/null +++ b/main_windows_webview_userdata.go @@ -0,0 +1,123 @@ +//go:build windows + +package main + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +func resolveWindowsWebviewUserDataPath() string { + appDataDir := strings.TrimSpace(os.Getenv("APPDATA")) + if appDataDir == "" { + return "" + } + + targetDir := filepath.Join(appDataDir, "GoNavi", "WebView2") + _ = migrateLegacyWindowsWebviewUserData(appDataDir, targetDir) + return targetDir +} + +func migrateLegacyWindowsWebviewUserData(appDataDir, targetDir string) error { + if dirHasContent(targetDir) { + return nil + } + + exeName := "GoNavi.exe" + if exePath, err := os.Executable(); err == nil { + base := strings.TrimSpace(filepath.Base(exePath)) + if base != "" { + exeName = base + } + } + exeBase := strings.TrimSuffix(exeName, filepath.Ext(exeName)) + + candidates := []string{ + filepath.Join(appDataDir, exeName), + filepath.Join(appDataDir, exeBase), + filepath.Join(appDataDir, "GoNavi.exe"), + filepath.Join(appDataDir, "GoNavi"), + } + + seen := make(map[string]struct{}, len(candidates)) + for _, candidate := range candidates { + src := filepath.Clean(strings.TrimSpace(candidate)) + if src == "" || strings.EqualFold(src, filepath.Clean(targetDir)) { + continue + } + key := strings.ToLower(src) + if _, exists := seen[key]; exists { + continue + } + seen[key] = struct{}{} + + if !dirHasContent(src) { + continue + } + return copyDirTree(src, targetDir) + } + return nil +} + +func dirHasContent(path string) bool { + info, err := os.Stat(path) + if err != nil || !info.IsDir() { + return false + } + entries, err := os.ReadDir(path) + return err == nil && len(entries) > 0 +} + +func copyDirTree(srcDir, dstDir string) error { + if err := os.MkdirAll(dstDir, 0o755); err != nil { + return err + } + + return filepath.WalkDir(srcDir, func(srcPath string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relPath, err := filepath.Rel(srcDir, srcPath) + if err != nil { + return err + } + if relPath == "." { + return nil + } + dstPath := filepath.Join(dstDir, relPath) + + if d.IsDir() { + return os.MkdirAll(dstPath, 0o755) + } + + info, err := d.Info() + if err != nil { + return err + } + return copyFileWithMode(srcPath, dstPath, info.Mode()) + }) +} + +func copyFileWithMode(srcPath, dstPath string, mode os.FileMode) error { + srcFile, err := os.Open(srcPath) + if err != nil { + return err + } + defer srcFile.Close() + + if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil { + return err + } + dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode.Perm()) + if err != nil { + return err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + return nil +} diff --git a/main_windows_webview_userdata_stub.go b/main_windows_webview_userdata_stub.go new file mode 100644 index 0000000..7dfe331 --- /dev/null +++ b/main_windows_webview_userdata_stub.go @@ -0,0 +1,7 @@ +//go:build !windows + +package main + +func resolveWindowsWebviewUserDataPath() string { + return "" +}