diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 035594c5df..6050f5a498 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -197,6 +197,8 @@ export enum IpcChannel { DataMigrate_RetryMigration = 'data-migrate:retry-migration', DataMigrate_RestartApp = 'data-migrate:restart-app', DataMigrate_CloseWindow = 'data-migrate:close-window', + DataMigrate_SendReduxData = 'data-migrate:send-redux-data', + DataMigrate_GetReduxData = 'data-migrate:get-redux-data', // zip Zip_Compress = 'zip:compress', diff --git a/packages/shared/data/preferences.ts b/packages/shared/data/preferences.ts index fd2f42ce2c..1b6de8402f 100644 --- a/packages/shared/data/preferences.ts +++ b/packages/shared/data/preferences.ts @@ -1,6 +1,6 @@ /** * Auto-generated preferences configuration - * Generated at: 2025-08-09T07:25:38.982Z + * Generated at: 2025-08-10T12:46:51.544Z * * This file is automatically generated from classification.json * To update this file, modify classification.json and run: @@ -27,7 +27,7 @@ export interface PreferencesType { // redux/settings/testPlan 'app.dist.test_plan.enabled': boolean // electronStore/Language/Language - 'app.language': unknown | null + 'app.language': string | null // redux/settings/launchOnBoot 'app.launch_on_boot': boolean // redux/settings/notification.assistant @@ -65,7 +65,7 @@ export interface PreferencesType { // redux/settings/userName 'app.user.name': string // electronStore/ZoomFactor/ZoomFactor - 'app.zoom_factor': unknown | null + 'app.zoom_factor': number // redux/settings/codeCollapsible 'chat.code.collapsible': boolean // redux/settings/codeEditor.autocompletion @@ -292,26 +292,22 @@ export interface PreferencesType { 'feature.selection.action_items': unknown[] // redux/selectionStore/actionWindowOpacity 'feature.selection.action_window_opacity': number + // redux/selectionStore/isAutoClose + 'feature.selection.auto_close': boolean + // redux/selectionStore/isAutoPin + 'feature.selection.auto_pin': boolean + // redux/selectionStore/isCompact + 'feature.selection.compact': boolean // redux/selectionStore/selectionEnabled 'feature.selection.enabled': boolean // redux/selectionStore/filterList 'feature.selection.filter_list': unknown[] // redux/selectionStore/filterMode 'feature.selection.filter_mode': string - // electronStore/SelectionAssistantFollowToolbar/SelectionAssistantFollowToolbar - 'feature.selection.follow_toolbar': unknown | null - // redux/selectionStore/isAutoClose - 'feature.selection.is_auto_close': boolean - // redux/selectionStore/isAutoPin - 'feature.selection.is_auto_pin': boolean - // redux/selectionStore/isCompact - 'feature.selection.is_compact': boolean // redux/selectionStore/isFollowToolbar - 'feature.selection.is_follow_toolbar': boolean + 'feature.selection.follow_toolbar': boolean // redux/selectionStore/isRemeberWinSize - 'feature.selection.is_remeber_win_size': boolean - // electronStore/SelectionAssistantRemeberWinSize/SelectionAssistantRemeberWinSize - 'feature.selection.remember_win_size': unknown | null + 'feature.selection.remember_win_size': boolean // redux/selectionStore/triggerMode 'feature.selection.trigger_mode': string // redux/settings/enableTopicNaming @@ -362,7 +358,7 @@ export const defaultPreferences: PreferencesType = { 'app.tray.on_launch': false, 'app.user.id': 'uuid()', 'app.user.name': '', - 'app.zoom_factor': null, + 'app.zoom_factor': 1, 'chat.code.collapsible': false, 'chat.code.editor.autocompletion': true, 'chat.code.editor.fold_gutter': false, @@ -476,16 +472,14 @@ export const defaultPreferences: PreferencesType = { 'feature.quick_assistant.read_clipboard_at_startup': true, 'feature.selection.action_items': [], 'feature.selection.action_window_opacity': 100, + 'feature.selection.auto_close': false, + 'feature.selection.auto_pin': false, + 'feature.selection.compact': false, 'feature.selection.enabled': false, 'feature.selection.filter_list': [], 'feature.selection.filter_mode': 'default', - 'feature.selection.follow_toolbar': null, - 'feature.selection.is_auto_close': false, - 'feature.selection.is_auto_pin': false, - 'feature.selection.is_compact': false, - 'feature.selection.is_follow_toolbar': true, - 'feature.selection.is_remeber_win_size': false, - 'feature.selection.remember_win_size': null, + 'feature.selection.follow_toolbar': true, + 'feature.selection.remember_win_size': false, 'feature.selection.trigger_mode': 'selected', 'topic.naming.enabled': true, 'topic.naming.prompt': '', @@ -503,8 +497,8 @@ export const defaultPreferences: PreferencesType = { /** * 生成统计: - * - 总配置项: 158 - * - electronStore项: 4 + * - 总配置项: 156 + * - electronStore项: 2 * - redux项: 154 * - localStorage项: 0 */ diff --git a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts index 16695c89f2..0b4375956f 100644 --- a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts +++ b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts @@ -57,6 +57,7 @@ class DataRefactorMigrateService { message: 'Ready to start data migration' } private isMigrating: boolean = false + private reduxData: any = null // Cache for Redux persist data constructor() { this.backupManager = new BackupManager() @@ -69,6 +70,24 @@ class DataRefactorMigrateService { return this.backupManager } + /** + * Get cached Redux persist data for migration + */ + public getReduxData(): any { + return this.reduxData + } + + /** + * Set Redux persist data from renderer process + */ + public setReduxData(data: any): void { + this.reduxData = data + logger.info('Redux data cached for migration', { + dataKeys: data ? Object.keys(data) : [], + hasData: !!data + }) + } + /** * Register migration-specific IPC handlers * This creates an isolated IPC environment only for migration operations @@ -226,6 +245,25 @@ class DataRefactorMigrateService { } }) + ipcMain.handle(IpcChannel.DataMigrate_SendReduxData, (_event, data) => { + try { + this.setReduxData(data) + return { success: true } + } catch (error) { + logger.error('IPC handler error: sendReduxData', error as Error) + throw error + } + }) + + ipcMain.handle(IpcChannel.DataMigrate_GetReduxData, () => { + try { + return this.getReduxData() + } catch (error) { + logger.error('IPC handler error: getReduxData', error as Error) + throw error + } + }) + logger.info('Migration IPC handlers registered successfully') } @@ -248,6 +286,8 @@ class DataRefactorMigrateService { ipcMain.removeAllListeners(IpcChannel.DataMigrate_RetryMigration) ipcMain.removeAllListeners(IpcChannel.DataMigrate_RestartApp) ipcMain.removeAllListeners(IpcChannel.DataMigrate_CloseWindow) + ipcMain.removeAllListeners(IpcChannel.DataMigrate_SendReduxData) + ipcMain.removeAllListeners(IpcChannel.DataMigrate_GetReduxData) logger.info('Migration IPC handlers unregistered successfully') } catch (error) { @@ -643,8 +683,8 @@ class DataRefactorMigrateService { try { logger.info('Executing migration') - // Create preferences migrator - const preferencesMigrator = new PreferencesMigrator() + // Create preferences migrator with reference to this service for Redux data access + const preferencesMigrator = new PreferencesMigrator(this) // Execute preferences migration with progress updates const result = await preferencesMigrator.migrate((progress, message) => { diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMappings.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMappings.ts new file mode 100644 index 0000000000..7fd87325dd --- /dev/null +++ b/src/main/data/migrate/dataRefactor/migrators/PreferencesMappings.ts @@ -0,0 +1,673 @@ +/** + * Auto-generated preference mappings from classification.json + * Generated at: 2025-08-10T12:47:40.247Z + * + * This file contains pure mapping relationships without default values. + * Default values are managed in packages/shared/data/preferences.ts + * + * === AUTO-GENERATED CONTENT START === + */ + +/** + * ElectronStore映射关系 - 简单一层结构 + * + * ElectronStore没有嵌套,originalKey直接对应configManager.get(key) + */ +export const ELECTRON_STORE_MAPPINGS = [ + { + originalKey: 'Language', + targetKey: 'app.language' + }, + { + originalKey: 'ZoomFactor', + targetKey: 'app.zoom_factor' + } +] as const + +/** + * Redux Store映射关系 - 按category分组,支持嵌套路径 + * + * Redux Store可能有children结构,originalKey可能包含嵌套路径: + * - 直接字段: "theme" -> reduxData.settings.theme + * - 嵌套字段: "codeEditor.enabled" -> reduxData.settings.codeEditor.enabled + * - 多层嵌套: "exportMenuOptions.docx" -> reduxData.settings.exportMenuOptions.docx + */ +export const REDUX_STORE_MAPPINGS = { + settings: [ + { + originalKey: 'autoCheckUpdate', + targetKey: 'app.dist.auto_update.enabled' + }, + { + originalKey: 'clickTrayToShowQuickAssistant', + targetKey: 'feature.quick_assistant.click_tray_to_show' + }, + { + originalKey: 'disableHardwareAcceleration', + targetKey: 'app.disable_hardware_acceleration' + }, + { + originalKey: 'enableDataCollection', + targetKey: 'app.privacy.data_collection.enabled' + }, + { + originalKey: 'enableDeveloperMode', + targetKey: 'app.developer_mode.enabled' + }, + { + originalKey: 'enableQuickAssistant', + targetKey: 'feature.quick_assistant.enabled' + }, + { + originalKey: 'launchToTray', + targetKey: 'app.tray.on_launch' + }, + { + originalKey: 'testChannel', + targetKey: 'app.dist.test_plan.channel' + }, + { + originalKey: 'testPlan', + targetKey: 'app.dist.test_plan.enabled' + }, + { + originalKey: 'theme', + targetKey: 'app.theme.mode' + }, + { + originalKey: 'tray', + targetKey: 'app.tray.enabled' + }, + { + originalKey: 'trayOnClose', + targetKey: 'app.tray.on_close' + }, + { + originalKey: 'sendMessageShortcut', + targetKey: 'chat.input.send_message_shortcut' + }, + { + originalKey: 'proxyMode', + targetKey: 'app.proxy.mode' + }, + { + originalKey: 'proxyUrl', + targetKey: 'app.proxy.url' + }, + { + originalKey: 'proxyBypassRules', + targetKey: 'app.proxy.bypass_rules' + }, + { + originalKey: 'userName', + targetKey: 'app.user.name' + }, + { + originalKey: 'userId', + targetKey: 'app.user.id' + }, + { + originalKey: 'showPrompt', + targetKey: 'chat.message.show_prompt' + }, + { + originalKey: 'showTokens', + targetKey: 'chat.message.show_tokens' + }, + { + originalKey: 'showMessageDivider', + targetKey: 'chat.message.show_divider' + }, + { + originalKey: 'messageFont', + targetKey: 'chat.message.font' + }, + { + originalKey: 'showInputEstimatedTokens', + targetKey: 'chat.input.show_estimated_tokens' + }, + { + originalKey: 'launchOnBoot', + targetKey: 'app.launch_on_boot' + }, + { + originalKey: 'userTheme', + targetKey: 'app.theme.user_defined' + }, + { + originalKey: 'windowStyle', + targetKey: 'app.theme.window_style' + }, + { + originalKey: 'fontSize', + targetKey: 'chat.message.font_size' + }, + { + originalKey: 'topicPosition', + targetKey: 'topic.position' + }, + { + originalKey: 'showTopicTime', + targetKey: 'topic.show_time' + }, + { + originalKey: 'pinTopicsToTop', + targetKey: 'topic.pin_to_top' + }, + { + originalKey: 'assistantIconType', + targetKey: 'ui.assistant_icon_type' + }, + { + originalKey: 'pasteLongTextAsFile', + targetKey: 'chat.input.paste_long_text_as_file' + }, + { + originalKey: 'pasteLongTextThreshold', + targetKey: 'chat.input.paste_long_text_threshold' + }, + { + originalKey: 'clickAssistantToShowTopic', + targetKey: 'ui.click_assistant_to_show_topic' + }, + { + originalKey: 'codeExecution.enabled', + targetKey: 'chat.code.execution.enabled' + }, + { + originalKey: 'codeExecution.timeoutMinutes', + targetKey: 'chat.code.execution.timeout_minutes' + }, + { + originalKey: 'codeEditor.enabled', + targetKey: 'chat.code.editor.highlight_active_line' + }, + { + originalKey: 'codeEditor.themeLight', + targetKey: 'chat.code.editor.theme_light' + }, + { + originalKey: 'codeEditor.themeDark', + targetKey: 'chat.code.editor.theme_dark' + }, + { + originalKey: 'codeEditor.foldGutter', + targetKey: 'chat.code.editor.fold_gutter' + }, + { + originalKey: 'codeEditor.autocompletion', + targetKey: 'chat.code.editor.autocompletion' + }, + { + originalKey: 'codeEditor.keymap', + targetKey: 'chat.code.editor.keymap' + }, + { + originalKey: 'codePreview.themeLight', + targetKey: 'chat.code.preview.theme_light' + }, + { + originalKey: 'codePreview.themeDark', + targetKey: 'chat.code.preview.theme_dark' + }, + { + originalKey: 'codeViewer.themeLight', + targetKey: 'chat.code.viewer.theme_light' + }, + { + originalKey: 'codeViewer.themeDark', + targetKey: 'chat.code.viewer.theme_dark' + }, + { + originalKey: 'codeShowLineNumbers', + targetKey: 'chat.code.show_line_numbers' + }, + { + originalKey: 'codeCollapsible', + targetKey: 'chat.code.collapsible' + }, + { + originalKey: 'codeWrappable', + targetKey: 'chat.code.wrappable' + }, + { + originalKey: 'codeImageTools', + targetKey: 'chat.code.image_tools' + }, + { + originalKey: 'mathEngine', + targetKey: 'chat.message.math_engine' + }, + { + originalKey: 'messageStyle', + targetKey: 'chat.message.style' + }, + { + originalKey: 'foldDisplayMode', + targetKey: 'chat.message.multi_model.fold_display_mode' + }, + { + originalKey: 'gridColumns', + targetKey: 'chat.message.multi_model.grid_columns' + }, + { + originalKey: 'gridPopoverTrigger', + targetKey: 'chat.message.multi_model.grid_popover_trigger' + }, + { + originalKey: 'messageNavigation', + targetKey: 'chat.message.navigation_mode' + }, + { + originalKey: 'skipBackupFile', + targetKey: 'data.backup.general.skip_backup_file' + }, + { + originalKey: 'webdavHost', + targetKey: 'data.backup.webdav.host' + }, + { + originalKey: 'webdavUser', + targetKey: 'data.backup.webdav.user' + }, + { + originalKey: 'webdavPass', + targetKey: 'data.backup.webdav.pass' + }, + { + originalKey: 'webdavPath', + targetKey: 'data.backup.webdav.path' + }, + { + originalKey: 'webdavAutoSync', + targetKey: 'data.backup.webdav.auto_sync' + }, + { + originalKey: 'webdavSyncInterval', + targetKey: 'data.backup.webdav.sync_interval' + }, + { + originalKey: 'webdavMaxBackups', + targetKey: 'data.backup.webdav.max_backups' + }, + { + originalKey: 'webdavSkipBackupFile', + targetKey: 'data.backup.webdav.skip_backup_file' + }, + { + originalKey: 'webdavDisableStream', + targetKey: 'data.backup.webdav.disable_stream' + }, + { + originalKey: 'autoTranslateWithSpace', + targetKey: 'chat.input.translate.auto_translate_with_space' + }, + { + originalKey: 'showTranslateConfirm', + targetKey: 'chat.input.translate.show_confirm' + }, + { + originalKey: 'enableTopicNaming', + targetKey: 'topic.naming.enabled' + }, + { + originalKey: 'customCss', + targetKey: 'ui.custom_css' + }, + { + originalKey: 'topicNamingPrompt', + targetKey: 'topic.naming.prompt' + }, + { + originalKey: 'narrowMode', + targetKey: 'chat.narrow_mode' + }, + { + originalKey: 'multiModelMessageStyle', + targetKey: 'chat.message.multi_model.style' + }, + { + originalKey: 'readClipboardAtStartup', + targetKey: 'feature.quick_assistant.read_clipboard_at_startup' + }, + { + originalKey: 'notionDatabaseID', + targetKey: 'data.integration.notion.database_id' + }, + { + originalKey: 'notionApiKey', + targetKey: 'data.integration.notion.api_key' + }, + { + originalKey: 'notionPageNameKey', + targetKey: 'data.integration.notion.page_name_key' + }, + { + originalKey: 'markdownExportPath', + targetKey: 'data.export.markdown.path' + }, + { + originalKey: 'forceDollarMathInMarkdown', + targetKey: 'data.export.markdown.force_dollar_math' + }, + { + originalKey: 'useTopicNamingForMessageTitle', + targetKey: 'data.export.markdown.use_topic_naming_for_message_title' + }, + { + originalKey: 'showModelNameInMarkdown', + targetKey: 'data.export.markdown.show_model_name' + }, + { + originalKey: 'showModelProviderInMarkdown', + targetKey: 'data.export.markdown.show_model_provider' + }, + { + originalKey: 'thoughtAutoCollapse', + targetKey: 'chat.message.thought.auto_collapse' + }, + { + originalKey: 'notionExportReasoning', + targetKey: 'data.integration.notion.export_reasoning' + }, + { + originalKey: 'excludeCitationsInExport', + targetKey: 'data.export.markdown.exclude_citations' + }, + { + originalKey: 'standardizeCitationsInExport', + targetKey: 'data.export.markdown.standardize_citations' + }, + { + originalKey: 'yuqueToken', + targetKey: 'data.integration.yuque.token' + }, + { + originalKey: 'yuqueUrl', + targetKey: 'data.integration.yuque.url' + }, + { + originalKey: 'yuqueRepoId', + targetKey: 'data.integration.yuque.repo_id' + }, + { + originalKey: 'joplinToken', + targetKey: 'data.integration.joplin.token' + }, + { + originalKey: 'joplinUrl', + targetKey: 'data.integration.joplin.url' + }, + { + originalKey: 'joplinExportReasoning', + targetKey: 'data.integration.joplin.export_reasoning' + }, + { + originalKey: 'defaultObsidianVault', + targetKey: 'data.integration.obsidian.default_vault' + }, + { + originalKey: 'siyuanApiUrl', + targetKey: 'data.integration.siyuan.api_url' + }, + { + originalKey: 'siyuanToken', + targetKey: 'data.integration.siyuan.token' + }, + { + originalKey: 'siyuanBoxId', + targetKey: 'data.integration.siyuan.box_id' + }, + { + originalKey: 'siyuanRootPath', + targetKey: 'data.integration.siyuan.root_path' + }, + { + originalKey: 'maxKeepAliveMinapps', + targetKey: 'feature.minapp.max_keep_alive' + }, + { + originalKey: 'showOpenedMinappsInSidebar', + targetKey: 'feature.minapp.show_opened_in_sidebar' + }, + { + originalKey: 'minappsOpenLinkExternal', + targetKey: 'feature.minapp.open_link_external' + }, + { + originalKey: 'enableSpellCheck', + targetKey: 'app.spell_check.enabled' + }, + { + originalKey: 'spellCheckLanguages', + targetKey: 'app.spell_check.languages' + }, + { + originalKey: 'enableQuickPanelTriggers', + targetKey: 'chat.input.quick_panel.triggers_enabled' + }, + { + originalKey: 'enableBackspaceDeleteModel', + targetKey: 'chat.input.backspace_delete_model' + }, + { + originalKey: 'exportMenuOptions.image', + targetKey: 'data.export.menus.image' + }, + { + originalKey: 'exportMenuOptions.markdown', + targetKey: 'data.export.menus.markdown' + }, + { + originalKey: 'exportMenuOptions.markdown_reason', + targetKey: 'data.export.menus.markdown_reason' + }, + { + originalKey: 'exportMenuOptions.notion', + targetKey: 'data.export.menus.notion' + }, + { + originalKey: 'exportMenuOptions.yuque', + targetKey: 'data.export.menus.yuque' + }, + { + originalKey: 'exportMenuOptions.joplin', + targetKey: 'data.export.menus.joplin' + }, + { + originalKey: 'exportMenuOptions.obsidian', + targetKey: 'data.export.menus.obsidian' + }, + { + originalKey: 'exportMenuOptions.siyuan', + targetKey: 'data.export.menus.siyuan' + }, + { + originalKey: 'exportMenuOptions.docx', + targetKey: 'data.export.menus.docx' + }, + { + originalKey: 'exportMenuOptions.plain_text', + targetKey: 'data.export.menus.plain_text' + }, + { + originalKey: 'notification.assistant', + targetKey: 'app.notification.assistant.enabled' + }, + { + originalKey: 'notification.backup', + targetKey: 'app.notification.backup.enabled' + }, + { + originalKey: 'notification.knowledge', + targetKey: 'app.notification.knowledge.enabled' + }, + { + originalKey: 'localBackupDir', + targetKey: 'data.backup.local.dir' + }, + { + originalKey: 'localBackupAutoSync', + targetKey: 'data.backup.local.auto_sync' + }, + { + originalKey: 'localBackupSyncInterval', + targetKey: 'data.backup.local.sync_interval' + }, + { + originalKey: 'localBackupMaxBackups', + targetKey: 'data.backup.local.max_backups' + }, + { + originalKey: 'localBackupSkipBackupFile', + targetKey: 'data.backup.local.skip_backup_file' + }, + { + originalKey: 's3.endpoint', + targetKey: 'data.backup.s3.endpoint' + }, + { + originalKey: 's3.region', + targetKey: 'data.backup.s3.region' + }, + { + originalKey: 's3.bucket', + targetKey: 'data.backup.s3.bucket' + }, + { + originalKey: 's3.accessKeyId', + targetKey: 'data.backup.s3.access_key_id' + }, + { + originalKey: 's3.secretAccessKey', + targetKey: 'data.backup.s3.secret_access_key' + }, + { + originalKey: 's3.root', + targetKey: 'data.backup.s3.root' + }, + { + originalKey: 's3.autoSync', + targetKey: 'data.backup.s3.auto_sync' + }, + { + originalKey: 's3.syncInterval', + targetKey: 'data.backup.s3.sync_interval' + }, + { + originalKey: 's3.maxBackups', + targetKey: 'data.backup.s3.max_backups' + }, + { + originalKey: 's3.skipBackupFile', + targetKey: 'data.backup.s3.skip_backup_file' + }, + { + originalKey: 'navbarPosition', + targetKey: 'ui.navbar.position' + }, + { + originalKey: 'apiServer.enabled', + targetKey: 'feature.csaas.enabled' + }, + { + originalKey: 'apiServer.host', + targetKey: 'feature.csaas.host' + }, + { + originalKey: 'apiServer.port', + targetKey: 'feature.csaas.port' + }, + { + originalKey: 'apiServer.apiKey', + targetKey: 'feature.csaas.api_key' + } + ], + selectionStore: [ + { + originalKey: 'selectionEnabled', + targetKey: 'feature.selection.enabled' + }, + { + originalKey: 'filterList', + targetKey: 'feature.selection.filter_list' + }, + { + originalKey: 'filterMode', + targetKey: 'feature.selection.filter_mode' + }, + { + originalKey: 'isFollowToolbar', + targetKey: 'feature.selection.follow_toolbar' + }, + { + originalKey: 'isRemeberWinSize', + targetKey: 'feature.selection.remember_win_size' + }, + { + originalKey: 'triggerMode', + targetKey: 'feature.selection.trigger_mode' + }, + { + originalKey: 'isCompact', + targetKey: 'feature.selection.compact' + }, + { + originalKey: 'isAutoClose', + targetKey: 'feature.selection.auto_close' + }, + { + originalKey: 'isAutoPin', + targetKey: 'feature.selection.auto_pin' + }, + { + originalKey: 'actionWindowOpacity', + targetKey: 'feature.selection.action_window_opacity' + }, + { + originalKey: 'actionItems', + targetKey: 'feature.selection.action_items' + } + ], + nutstore: [ + { + originalKey: 'nutstoreToken', + targetKey: 'data.backup.nutstore.token' + }, + { + originalKey: 'nutstorePath', + targetKey: 'data.backup.nutstore.path' + }, + { + originalKey: 'nutstoreAutoSync', + targetKey: 'data.backup.nutstore.auto_sync' + }, + { + originalKey: 'nutstoreSyncInterval', + targetKey: 'data.backup.nutstore.sync_interval' + }, + { + originalKey: 'nutstoreSyncState', + targetKey: 'data.backup.nutstore.sync_state' + }, + { + originalKey: 'nutstoreSkipBackupFile', + targetKey: 'data.backup.nutstore.skip_backup_file' + } + ] +} as const + +// === AUTO-GENERATED CONTENT END === + +/** + * 映射统计: + * - ElectronStore项: 2 + * - Redux Store项: 154 + * - Redux分类: settings, selectionStore, nutstore + * - 总配置项: 156 + * + * 使用说明: + * 1. ElectronStore读取: configManager.get(mapping.originalKey) + * 2. Redux读取: 需要解析嵌套路径 reduxData[category][originalKey路径] + * 3. 默认值: 从defaultPreferences.default[mapping.targetKey]获取 + */ diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts index e38a0f1508..3023f3d341 100644 --- a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts +++ b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts @@ -1,9 +1,11 @@ import dbService from '@data/db/DbService' import { preferenceTable } from '@data/db/schemas/preference' import { loggerService } from '@logger' +import { defaultPreferences } from '@shared/data/preferences' import { and, eq } from 'drizzle-orm' import { configManager } from '../../../../services/ConfigManager' +import { ELECTRON_STORE_MAPPINGS, REDUX_STORE_MAPPINGS } from './PreferencesMappings' const logger = loggerService.withContext('PreferencesMigrator') @@ -13,7 +15,7 @@ export interface MigrationItem { type: string defaultValue: any source: 'electronStore' | 'redux' - sourceCategory: string + sourceCategory?: string // Optional for electronStore } export interface MigrationResult { @@ -27,6 +29,11 @@ export interface MigrationResult { export class PreferencesMigrator { private db = dbService.getDb() + private migrateService: any // Reference to DataRefactorMigrateService + + constructor(migrateService?: any) { + this.migrateService = migrateService + } /** * Execute preferences migration from all sources @@ -43,6 +50,7 @@ export class PreferencesMigrator { try { // Get migration items from classification.json const migrationItems = await this.loadMigrationItems() + const totalItems = migrationItems.length logger.info(`Found ${totalItems} items to migrate`) @@ -83,70 +91,45 @@ export class PreferencesMigrator { } /** - * Load migration items from the generated preferences.ts mappings - * For now, we'll use a simplified set based on the current generated migration code + * Load migration items from generated mapping relationships + * This uses the auto-generated PreferencesMappings.ts file */ private async loadMigrationItems(): Promise { - // This is a simplified implementation. In the full version, this would read from - // the classification.json and apply the same deduplication logic as the generators + logger.info('Loading migration items from generated mappings') + const items: MigrationItem[] = [] - const items: MigrationItem[] = [ - // ElectronStore items (from generated migration code) - { - originalKey: 'Language', - targetKey: 'app.language', - sourceCategory: 'Language', - type: 'unknown', - defaultValue: null, + // Process ElectronStore mappings - no sourceCategory needed + ELECTRON_STORE_MAPPINGS.forEach((mapping) => { + const defaultValue = defaultPreferences.default[mapping.targetKey] ?? null + items.push({ + originalKey: mapping.originalKey, + targetKey: mapping.targetKey, + type: 'unknown', // Type will be inferred from defaultValue during conversion + defaultValue, source: 'electronStore' - }, - { - originalKey: 'SelectionAssistantFollowToolbar', - targetKey: 'feature.selection.follow_toolbar', - sourceCategory: 'SelectionAssistantFollowToolbar', - type: 'unknown', - defaultValue: null, - source: 'electronStore' - }, - { - originalKey: 'SelectionAssistantRemeberWinSize', - targetKey: 'feature.selection.remember_win_size', - sourceCategory: 'SelectionAssistantRemeberWinSize', - type: 'unknown', - defaultValue: null, - source: 'electronStore' - }, - { - originalKey: 'ZoomFactor', - targetKey: 'app.zoom_factor', - sourceCategory: 'ZoomFactor', - type: 'unknown', - defaultValue: null, - source: 'electronStore' - } - ] + }) + }) - // Add some sample Redux items (in full implementation, these would be loaded from classification.json) - const reduxItems: MigrationItem[] = [ - { - originalKey: 'theme', - targetKey: 'app.theme.mode', - sourceCategory: 'settings', - type: 'string', - defaultValue: 'ThemeMode.system', - source: 'redux' - }, - { - originalKey: 'language', - targetKey: 'app.language', - sourceCategory: 'settings', - type: 'string', - defaultValue: 'en', - source: 'redux' - } - ] + // Process Redux mappings + Object.entries(REDUX_STORE_MAPPINGS).forEach(([category, mappings]) => { + mappings.forEach((mapping) => { + const defaultValue = defaultPreferences.default[mapping.targetKey] ?? null + items.push({ + originalKey: mapping.originalKey, // May contain nested paths like "codeEditor.enabled" + targetKey: mapping.targetKey, + sourceCategory: category, + type: 'unknown', // Type will be inferred from defaultValue during conversion + defaultValue, + source: 'redux' + }) + }) + }) - items.push(...reduxItems) + logger.info('Successfully loaded migration items from generated mappings', { + totalItems: items.length, + electronStoreItems: items.filter((i) => i.source === 'electronStore').length, + reduxItems: items.filter((i) => i.source === 'redux').length + }) return items } @@ -163,28 +146,80 @@ export class PreferencesMigrator { if (item.source === 'electronStore') { originalValue = await this.readFromElectronStore(item.originalKey) } else if (item.source === 'redux') { + if (!item.sourceCategory) { + throw new Error(`Redux source requires sourceCategory for item: ${item.originalKey}`) + } originalValue = await this.readFromReduxPersist(item.sourceCategory, item.originalKey) } else { throw new Error(`Unknown source: ${item.source}`) } - // Use default value if original value is not found + // IMPORTANT: Only migrate if we actually found data, or if we want to set defaults + // Skip migration if no original data found and no meaningful default let valueToMigrate = originalValue + let shouldSkipMigration = false + if (originalValue === undefined || originalValue === null) { - valueToMigrate = item.defaultValue + // Check if we have a meaningful default value (not null) + if (item.defaultValue !== null && item.defaultValue !== undefined) { + valueToMigrate = item.defaultValue + logger.info('Using default value for migration', { + targetKey: item.targetKey, + defaultValue: item.defaultValue, + source: item.source, + originalKey: item.originalKey + }) + } else { + // Skip migration if no data found and no meaningful default + shouldSkipMigration = true + logger.info('Skipping migration - no data found and no meaningful default', { + targetKey: item.targetKey, + originalValue, + defaultValue: item.defaultValue, + source: item.source, + originalKey: item.originalKey + }) + } + } else { + // Found original data, log the successful data retrieval + logger.info('Found original data for migration', { + targetKey: item.targetKey, + source: item.source, + originalKey: item.originalKey, + valueType: typeof originalValue, + valuePreview: JSON.stringify(originalValue).substring(0, 100) + }) + } + + if (shouldSkipMigration) { + return } // Convert value to appropriate type const convertedValue = this.convertValue(valueToMigrate, item.type) // Write to preferences table using Drizzle - await this.writeToPreferences(item.targetKey, convertedValue) + try { + await this.writeToPreferences(item.targetKey, convertedValue) - logger.debug('Successfully migrated preference item', { - targetKey: item.targetKey, - originalValue, - convertedValue - }) + logger.info('Successfully migrated preference item', { + targetKey: item.targetKey, + source: item.source, + originalKey: item.originalKey, + originalValue, + convertedValue, + migrationSuccessful: true + }) + } catch (writeError) { + logger.error('Failed to write preference to database', { + targetKey: item.targetKey, + source: item.source, + originalKey: item.originalKey, + convertedValue, + writeError + }) + throw writeError + } } /** @@ -200,18 +235,104 @@ export class PreferencesMigrator { } /** - * Read value from Redux persist data + * Read value from Redux persist data with support for nested paths */ private async readFromReduxPersist(category: string, key: string): Promise { try { - // This is a simplified implementation - // In the full version, we would need to properly parse the leveldb files - // For now, we'll return undefined to use default values + // Get cached Redux data from migrate service + const reduxData = this.migrateService?.getReduxData() - logger.debug('Redux persist read not fully implemented', { category, key }) - return undefined + if (!reduxData) { + logger.warn('No Redux persist data available in cache', { category, key }) + return undefined + } + + logger.debug('Reading from cached Redux persist data', { + category, + key, + availableCategories: Object.keys(reduxData), + isNestedKey: key.includes('.') + }) + + // Get the category data from Redux persist cache + const categoryData = reduxData[category] + if (!categoryData) { + logger.debug('Category not found in Redux persist data', { + category, + availableCategories: Object.keys(reduxData) + }) + return undefined + } + + // Redux persist usually stores data as JSON strings + let parsedCategoryData + try { + parsedCategoryData = typeof categoryData === 'string' ? JSON.parse(categoryData) : categoryData + } catch (parseError) { + logger.warn('Failed to parse Redux persist category data', { + category, + categoryData: typeof categoryData, + parseError + }) + return undefined + } + + // Handle nested paths (e.g., "codeEditor.enabled") + let value + if (key.includes('.')) { + // Parse nested path + const keyPath = key.split('.') + let current = parsedCategoryData + + logger.debug('Parsing nested key path', { + category, + key, + keyPath, + rootDataKeys: current ? Object.keys(current) : [] + }) + + for (const pathSegment of keyPath) { + if (current && typeof current === 'object' && !Array.isArray(current)) { + current = current[pathSegment] + logger.debug('Navigated to path segment', { + pathSegment, + foundValue: current !== undefined, + valueType: typeof current + }) + } else { + logger.debug('Failed to navigate nested path - invalid structure', { + pathSegment, + currentType: typeof current, + isArray: Array.isArray(current) + }) + return undefined + } + } + value = current + } else { + // Direct field access (e.g., "theme") + value = parsedCategoryData[key] + } + + if (value !== undefined) { + logger.debug('Successfully read from Redux persist cache', { + category, + key, + value, + valueType: typeof value, + isNested: key.includes('.') + }) + } else { + logger.debug('Key not found in Redux persist data', { + category, + key, + availableKeys: parsedCategoryData ? Object.keys(parsedCategoryData) : [] + }) + } + + return value } catch (error) { - logger.warn('Failed to read from Redux persist', { category, key, error }) + logger.warn('Failed to read from Redux persist cache', { category, key, error }) return undefined } } diff --git a/src/main/data/migrate/dataRefactor/utils/migrationHelpers.ts b/src/main/data/migrate/dataRefactor/utils/migrationHelpers.ts deleted file mode 100644 index c63143f71f..0000000000 --- a/src/main/data/migrate/dataRefactor/utils/migrationHelpers.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Migration helper utilities - * Generated at: 2025-08-09T07:20:05.912Z - */ - -import { loggerService } from '@logger' - -const logger = loggerService.withContext('MigrationHelpers') - -export interface BackupInfo { - timestamp: string - version: string - dataSize: number - backupPath: string -} - -export class MigrationHelpers { - /** - * 创建数据备份 - */ - static async createBackup(): Promise { - logger.info('开始创建数据备份') - - // 实现备份逻辑 - const timestamp = new Date().toISOString() - const backupInfo: BackupInfo = { - timestamp, - version: process.env.npm_package_version || 'unknown', - dataSize: 0, - backupPath: '' - } - - // TODO: 实现具体的备份逻辑 - - logger.info('数据备份完成', backupInfo) - return backupInfo - } - - /** - * 验证备份完整性 - */ - static async validateBackup(backupInfo: BackupInfo): Promise { - logger.info('验证备份完整性', backupInfo) - - // TODO: 实现备份验证逻辑 - - return true - } - - /** - * 恢复备份 - */ - static async restoreBackup(backupInfo: BackupInfo): Promise { - logger.info('开始恢复备份', backupInfo) - - // TODO: 实现备份恢复逻辑 - - logger.info('备份恢复完成') - return true - } - - /** - * 清理临时文件 - */ - static async cleanup(): Promise { - logger.info('清理迁移临时文件') - - // TODO: 实现清理逻辑 - - logger.info('清理完成') - } -} \ No newline at end of file diff --git a/src/main/data/migrate/dataRefactor/utils/typeConverters.ts b/src/main/data/migrate/dataRefactor/utils/typeConverters.ts deleted file mode 100644 index f0c2e70b01..0000000000 --- a/src/main/data/migrate/dataRefactor/utils/typeConverters.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Type conversion utilities for migration - * Generated at: 2025-08-09T07:20:05.912Z - */ - -import { loggerService } from '@logger' - -const logger = loggerService.withContext('TypeConverter') - -export class TypeConverter { - /** - * 转换值到指定类型 - */ - convert(value: any, targetType: string): any { - if (value === null || value === undefined) { - return null - } - - try { - switch (targetType) { - case 'boolean': - return this.toBoolean(value) - - case 'string': - return this.toString(value) - - case 'number': - return this.toNumber(value) - - case 'array': - case 'unknown[]': - return this.toArray(value) - - case 'object': - case 'Record': - return this.toObject(value) - - default: - // 未知类型,保持原样 - logger.debug('未知类型,保持原值', { targetType, value }) - return value - } - } catch (error) { - logger.error('类型转换失败', { value, targetType, error }) - return value - } - } - - private toBoolean(value: any): boolean { - if (typeof value === 'boolean') { - return value - } - if (typeof value === 'string') { - const lower = value.toLowerCase() - return lower === 'true' || lower === '1' || lower === 'yes' - } - if (typeof value === 'number') { - return value !== 0 - } - return Boolean(value) - } - - private toString(value: any): string { - if (typeof value === 'string') { - return value - } - if (typeof value === 'number' || typeof value === 'boolean') { - return String(value) - } - if (typeof value === 'object') { - return JSON.stringify(value) - } - return String(value) - } - - private toNumber(value: any): number { - if (typeof value === 'number') { - return value - } - if (typeof value === 'string') { - const parsed = parseFloat(value) - if (isNaN(parsed)) { - logger.warn('字符串无法转换为数字', { value }) - return 0 - } - return parsed - } - if (typeof value === 'boolean') { - return value ? 1 : 0 - } - return 0 - } - - private toArray(value: any): any[] { - if (Array.isArray(value)) { - return value - } - if (typeof value === 'string') { - try { - const parsed = JSON.parse(value) - return Array.isArray(parsed) ? parsed : [value] - } catch { - return [value] - } - } - return [value] - } - - private toObject(value: any): Record { - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - return value - } - if (typeof value === 'string') { - try { - const parsed = JSON.parse(value) - return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) - ? parsed - : { value } - } catch { - return { value } - } - } - return { value } - } -} \ No newline at end of file diff --git a/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx b/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx index 3e93807d30..60c28de158 100644 --- a/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx +++ b/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx @@ -1,10 +1,13 @@ import { AppLogo } from '@renderer/config/env' +import { loggerService } from '@renderer/services/LoggerService' import { IpcChannel } from '@shared/IpcChannel' import { Button, Progress, Space, Steps } from 'antd' import { AlertTriangle, CheckCircle, Database, Loader2, Rocket } from 'lucide-react' import React, { useEffect, useMemo, useState } from 'react' import styled from 'styled-components' +const logger = loggerService.withContext('MigrateApp') + type MigrationStage = | 'introduction' // Introduction phase - user can cancel | 'backup_required' // Backup required - show backup requirement @@ -78,12 +81,68 @@ const MigrateApp: React.FC = () => { return 'process' }, [progress.stage]) + /** + * Extract Redux persist data from localStorage and send to main process + */ + const extractAndSendReduxData = async () => { + try { + logger.info('Extracting Redux persist data for migration...') + + // Get the Redux persist key (this should match the key used in store configuration) + const persistKey = 'persist:cherry-studio' + + // Read the persisted data from localStorage + const persistedDataString = localStorage.getItem(persistKey) + + if (!persistedDataString) { + logger.warn('No Redux persist data found in localStorage', { persistKey }) + // Send empty data to indicate no Redux data available + await window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_SendReduxData, null) + return + } + + // Parse the persisted data + const persistedData = JSON.parse(persistedDataString) + + logger.info('Found Redux persist data:', { + keys: Object.keys(persistedData), + hasData: !!persistedData, + dataSize: persistedDataString.length, + persistKey + }) + + // Send the Redux data to main process for migration + const result = await window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_SendReduxData, persistedData) + + if (result?.success) { + logger.info('Successfully sent Redux data to main process for migration') + } else { + logger.warn('Failed to send Redux data to main process', { result }) + } + } catch (error) { + logger.error('Error extracting Redux persist data', error as Error) + // Send null to indicate extraction failed + await window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_SendReduxData, null) + throw error + } + } + const handleProceedToBackup = () => { window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_ProceedToBackup) } - const handleStartMigration = () => { - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartMigration) + const handleStartMigration = async () => { + try { + // First, extract Redux persist data and send to main process + await extractAndSendReduxData() + + // Then start the migration process + window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartMigration) + } catch (error) { + logger.error('Failed to extract Redux data for migration', error as Error) + // Still proceed with migration even if Redux data extraction fails + window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartMigration) + } } const handleRestartApp = () => { @@ -256,6 +315,22 @@ const MigrateApp: React.FC = () => {
我们会指导你完成迁移,迁移过程不会损坏原来的数据,你随时可以取消迁移,并继续使用旧版本。 + {/* Debug button to test Redux data extraction */} +
+ +
)} diff --git a/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx b/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx index c5cd7a55ea..b50330fe8f 100644 --- a/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx +++ b/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx @@ -1,10 +1,13 @@ import '@ant-design/v5-patch-for-react-19' import '@renderer/assets/styles/index.scss' +import { loggerService } from '@logger' import { createRoot } from 'react-dom/client' import MigrateApp from './MigrateApp' +loggerService.initWindowSource('MigrateApp') + const root = createRoot(document.getElementById('root') as HTMLElement) root.render()