mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 23:12:38 +08:00
feat(migration): add Redux data handling to migration process
This commit introduces new IPC channels for sending and retrieving Redux persist data during the migration process. It enhances the DataRefactorMigrateService with methods to cache and manage Redux data, ensuring a smoother migration experience. Additionally, the MigrateApp component is updated to extract Redux data from localStorage and send it to the main process, improving the overall migration flow and user experience.
This commit is contained in:
parent
39257f64b1
commit
c217a0bf02
@ -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',
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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]获取
|
||||
*/
|
||||
@ -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<MigrationItem[]> {
|
||||
// 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<any> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<BackupInfo> {
|
||||
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<boolean> {
|
||||
logger.info('验证备份完整性', backupInfo)
|
||||
|
||||
// TODO: 实现备份验证逻辑
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复备份
|
||||
*/
|
||||
static async restoreBackup(backupInfo: BackupInfo): Promise<boolean> {
|
||||
logger.info('开始恢复备份', backupInfo)
|
||||
|
||||
// TODO: 实现备份恢复逻辑
|
||||
|
||||
logger.info('备份恢复完成')
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*/
|
||||
static async cleanup(): Promise<void> {
|
||||
logger.info('清理迁移临时文件')
|
||||
|
||||
// TODO: 实现清理逻辑
|
||||
|
||||
logger.info('清理完成')
|
||||
}
|
||||
}
|
||||
@ -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<string, unknown>':
|
||||
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<string, any> {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@ -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 = () => {
|
||||
<br />
|
||||
我们会指导你完成迁移,迁移过程不会损坏原来的数据,你随时可以取消迁移,并继续使用旧版本。
|
||||
</InfoDescription>
|
||||
{/* Debug button to test Redux data extraction */}
|
||||
<div style={{ marginTop: '24px', textAlign: 'center' }}>
|
||||
<Button
|
||||
size="small"
|
||||
type="dashed"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await extractAndSendReduxData()
|
||||
alert('Redux数据提取成功!请查看应用日志。')
|
||||
} catch (error) {
|
||||
alert('Redux数据提取失败:' + (error as Error).message)
|
||||
}
|
||||
}}>
|
||||
测试Redux数据提取
|
||||
</Button>
|
||||
</div>
|
||||
</InfoCard>
|
||||
)}
|
||||
|
||||
|
||||
@ -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(<MigrateApp />)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user