diff --git a/packages/shared/data/preferences.ts b/packages/shared/data/preferences.ts index 3da0ea4b59..fd2f42ce2c 100644 --- a/packages/shared/data/preferences.ts +++ b/packages/shared/data/preferences.ts @@ -1,35 +1,510 @@ /** - * we should use sorted object keys - * use `eslint --fix` to auto sort keys + * Auto-generated preferences configuration + * Generated at: 2025-08-09T07:25:38.982Z + * + * This file is automatically generated from classification.json + * To update this file, modify classification.json and run: + * node .claude/data-classify/scripts/generate-preferences.js + * + * === AUTO-GENERATED CONTENT START === */ /* eslint @typescript-eslint/member-ordering: ["error", { "interfaces": { "order": "alphabetically" }, "typeLiterals": { "order": "alphabetically" } }] */ + export interface PreferencesType { default: { - 'app.test5': { - content1: string - content2: number - } - 'sys.a.test3': boolean - 'sys.a.test4': string[] - 'ui.a.test1': string - 'ui.b.test2': number + // redux/settings/enableDeveloperMode + 'app.developer_mode.enabled': boolean + // redux/settings/disableHardwareAcceleration + 'app.disable_hardware_acceleration': boolean + // redux/settings/autoCheckUpdate + 'app.dist.auto_update.enabled': boolean + // redux/settings/testChannel + 'app.dist.test_plan.channel': string + // redux/settings/testPlan + 'app.dist.test_plan.enabled': boolean + // electronStore/Language/Language + 'app.language': unknown | null + // redux/settings/launchOnBoot + 'app.launch_on_boot': boolean + // redux/settings/notification.assistant + 'app.notification.assistant.enabled': boolean + // redux/settings/notification.backup + 'app.notification.backup.enabled': boolean + // redux/settings/notification.knowledge + 'app.notification.knowledge.enabled': boolean + // redux/settings/enableDataCollection + 'app.privacy.data_collection.enabled': boolean + // redux/settings/proxyBypassRules + 'app.proxy.bypass_rules': string | null + // redux/settings/proxyMode + 'app.proxy.mode': string + // redux/settings/proxyUrl + 'app.proxy.url': string | null + // redux/settings/enableSpellCheck + 'app.spell_check.enabled': boolean + // redux/settings/spellCheckLanguages + 'app.spell_check.languages': unknown[] + // redux/settings/theme + 'app.theme.mode': string + // redux/settings/userTheme + 'app.theme.user_defined': Record + // redux/settings/windowStyle + 'app.theme.window_style': string + // redux/settings/tray + 'app.tray.enabled': boolean + // redux/settings/trayOnClose + 'app.tray.on_close': boolean + // redux/settings/launchToTray + 'app.tray.on_launch': boolean + // redux/settings/userId + 'app.user.id': string + // redux/settings/userName + 'app.user.name': string + // electronStore/ZoomFactor/ZoomFactor + 'app.zoom_factor': unknown | null + // redux/settings/codeCollapsible + 'chat.code.collapsible': boolean + // redux/settings/codeEditor.autocompletion + 'chat.code.editor.autocompletion': boolean + // redux/settings/codeEditor.foldGutter + 'chat.code.editor.fold_gutter': boolean + // redux/settings/codeEditor.enabled + 'chat.code.editor.highlight_active_line': boolean + // redux/settings/codeEditor.keymap + 'chat.code.editor.keymap': boolean + // redux/settings/codeEditor.themeDark + 'chat.code.editor.theme_dark': string + // redux/settings/codeEditor.themeLight + 'chat.code.editor.theme_light': string + // redux/settings/codeExecution.enabled + 'chat.code.execution.enabled': boolean + // redux/settings/codeExecution.timeoutMinutes + 'chat.code.execution.timeout_minutes': number + // redux/settings/codeImageTools + 'chat.code.image_tools': boolean + // redux/settings/codePreview.themeDark + 'chat.code.preview.theme_dark': string + // redux/settings/codePreview.themeLight + 'chat.code.preview.theme_light': string + // redux/settings/codeShowLineNumbers + 'chat.code.show_line_numbers': boolean + // redux/settings/codeViewer.themeDark + 'chat.code.viewer.theme_dark': string + // redux/settings/codeViewer.themeLight + 'chat.code.viewer.theme_light': string + // redux/settings/codeWrappable + 'chat.code.wrappable': boolean + // redux/settings/enableBackspaceDeleteModel + 'chat.input.backspace_delete_model': boolean + // redux/settings/pasteLongTextAsFile + 'chat.input.paste_long_text_as_file': boolean + // redux/settings/pasteLongTextThreshold + 'chat.input.paste_long_text_threshold': number + // redux/settings/enableQuickPanelTriggers + 'chat.input.quick_panel.triggers_enabled': boolean + // redux/settings/sendMessageShortcut + 'chat.input.send_message_shortcut': string + // redux/settings/showInputEstimatedTokens + 'chat.input.show_estimated_tokens': boolean + // redux/settings/autoTranslateWithSpace + 'chat.input.translate.auto_translate_with_space': boolean + // redux/settings/showTranslateConfirm + 'chat.input.translate.show_confirm': boolean + // redux/settings/messageFont + 'chat.message.font': string + // redux/settings/fontSize + 'chat.message.font_size': number + // redux/settings/mathEngine + 'chat.message.math_engine': string + // redux/settings/foldDisplayMode + 'chat.message.multi_model.fold_display_mode': string + // redux/settings/gridColumns + 'chat.message.multi_model.grid_columns': number + // redux/settings/gridPopoverTrigger + 'chat.message.multi_model.grid_popover_trigger': string + // redux/settings/multiModelMessageStyle + 'chat.message.multi_model.style': string + // redux/settings/messageNavigation + 'chat.message.navigation_mode': string + // redux/settings/showMessageDivider + 'chat.message.show_divider': boolean + // redux/settings/showPrompt + 'chat.message.show_prompt': boolean + // redux/settings/showTokens + 'chat.message.show_tokens': boolean + // redux/settings/messageStyle + 'chat.message.style': string + // redux/settings/thoughtAutoCollapse + 'chat.message.thought.auto_collapse': boolean + // redux/settings/narrowMode + 'chat.narrow_mode': boolean + // redux/settings/skipBackupFile + 'data.backup.general.skip_backup_file': boolean + // redux/settings/localBackupAutoSync + 'data.backup.local.auto_sync': boolean + // redux/settings/localBackupDir + 'data.backup.local.dir': string + // redux/settings/localBackupMaxBackups + 'data.backup.local.max_backups': number + // redux/settings/localBackupSkipBackupFile + 'data.backup.local.skip_backup_file': boolean + // redux/settings/localBackupSyncInterval + 'data.backup.local.sync_interval': number + // redux/nutstore/nutstoreAutoSync + 'data.backup.nutstore.auto_sync': boolean | null + // redux/nutstore/nutstorePath + 'data.backup.nutstore.path': string | null + // redux/nutstore/nutstoreSkipBackupFile + 'data.backup.nutstore.skip_backup_file': boolean | null + // redux/nutstore/nutstoreSyncInterval + 'data.backup.nutstore.sync_interval': number | null + // redux/nutstore/nutstoreSyncState + 'data.backup.nutstore.sync_state': Record | null + // redux/nutstore/nutstoreToken + 'data.backup.nutstore.token': string | null + // redux/settings/s3.accessKeyId + 'data.backup.s3.access_key_id': string + // redux/settings/s3.autoSync + 'data.backup.s3.auto_sync': boolean + // redux/settings/s3.bucket + 'data.backup.s3.bucket': string + // redux/settings/s3.endpoint + 'data.backup.s3.endpoint': string + // redux/settings/s3.maxBackups + 'data.backup.s3.max_backups': number + // redux/settings/s3.region + 'data.backup.s3.region': string + // redux/settings/s3.root + 'data.backup.s3.root': string + // redux/settings/s3.secretAccessKey + 'data.backup.s3.secret_access_key': string + // redux/settings/s3.skipBackupFile + 'data.backup.s3.skip_backup_file': boolean + // redux/settings/s3.syncInterval + 'data.backup.s3.sync_interval': number + // redux/settings/webdavAutoSync + 'data.backup.webdav.auto_sync': boolean + // redux/settings/webdavDisableStream + 'data.backup.webdav.disable_stream': boolean + // redux/settings/webdavHost + 'data.backup.webdav.host': string + // redux/settings/webdavMaxBackups + 'data.backup.webdav.max_backups': number + // redux/settings/webdavPass + 'data.backup.webdav.pass': string + // redux/settings/webdavPath + 'data.backup.webdav.path': string + // redux/settings/webdavSkipBackupFile + 'data.backup.webdav.skip_backup_file': boolean + // redux/settings/webdavSyncInterval + 'data.backup.webdav.sync_interval': number + // redux/settings/webdavUser + 'data.backup.webdav.user': string + // redux/settings/excludeCitationsInExport + 'data.export.markdown.exclude_citations': boolean + // redux/settings/forceDollarMathInMarkdown + 'data.export.markdown.force_dollar_math': boolean + // redux/settings/markdownExportPath + 'data.export.markdown.path': string | null + // redux/settings/showModelNameInMarkdown + 'data.export.markdown.show_model_name': boolean + // redux/settings/showModelProviderInMarkdown + 'data.export.markdown.show_model_provider': boolean + // redux/settings/standardizeCitationsInExport + 'data.export.markdown.standardize_citations': boolean + // redux/settings/useTopicNamingForMessageTitle + 'data.export.markdown.use_topic_naming_for_message_title': boolean + // redux/settings/exportMenuOptions.docx + 'data.export.menus.docx': boolean + // redux/settings/exportMenuOptions.image + 'data.export.menus.image': boolean + // redux/settings/exportMenuOptions.joplin + 'data.export.menus.joplin': boolean + // redux/settings/exportMenuOptions.markdown + 'data.export.menus.markdown': boolean + // redux/settings/exportMenuOptions.markdown_reason + 'data.export.menus.markdown_reason': boolean + // redux/settings/exportMenuOptions.notion + 'data.export.menus.notion': boolean + // redux/settings/exportMenuOptions.obsidian + 'data.export.menus.obsidian': boolean + // redux/settings/exportMenuOptions.plain_text + 'data.export.menus.plain_text': boolean + // redux/settings/exportMenuOptions.siyuan + 'data.export.menus.siyuan': boolean + // redux/settings/exportMenuOptions.yuque + 'data.export.menus.yuque': boolean + // redux/settings/joplinExportReasoning + 'data.integration.joplin.export_reasoning': boolean + // redux/settings/joplinToken + 'data.integration.joplin.token': string + // redux/settings/joplinUrl + 'data.integration.joplin.url': string + // redux/settings/notionApiKey + 'data.integration.notion.api_key': string + // redux/settings/notionDatabaseID + 'data.integration.notion.database_id': string + // redux/settings/notionExportReasoning + 'data.integration.notion.export_reasoning': boolean + // redux/settings/notionPageNameKey + 'data.integration.notion.page_name_key': string + // redux/settings/defaultObsidianVault + 'data.integration.obsidian.default_vault': string | null + // redux/settings/siyuanApiUrl + 'data.integration.siyuan.api_url': string | null + // redux/settings/siyuanBoxId + 'data.integration.siyuan.box_id': string | null + // redux/settings/siyuanRootPath + 'data.integration.siyuan.root_path': string | null + // redux/settings/siyuanToken + 'data.integration.siyuan.token': string | null + // redux/settings/yuqueRepoId + 'data.integration.yuque.repo_id': string + // redux/settings/yuqueToken + 'data.integration.yuque.token': string + // redux/settings/yuqueUrl + 'data.integration.yuque.url': string + // redux/settings/apiServer.apiKey + 'feature.csaas.api_key': string + // redux/settings/apiServer.enabled + 'feature.csaas.enabled': boolean + // redux/settings/apiServer.host + 'feature.csaas.host': string + // redux/settings/apiServer.port + 'feature.csaas.port': number + // redux/settings/maxKeepAliveMinapps + 'feature.minapp.max_keep_alive': number + // redux/settings/minappsOpenLinkExternal + 'feature.minapp.open_link_external': boolean + // redux/settings/showOpenedMinappsInSidebar + 'feature.minapp.show_opened_in_sidebar': boolean + // redux/settings/clickTrayToShowQuickAssistant + 'feature.quick_assistant.click_tray_to_show': boolean + // redux/settings/enableQuickAssistant + 'feature.quick_assistant.enabled': boolean + // redux/settings/readClipboardAtStartup + 'feature.quick_assistant.read_clipboard_at_startup': boolean + // redux/selectionStore/actionItems + 'feature.selection.action_items': unknown[] + // redux/selectionStore/actionWindowOpacity + 'feature.selection.action_window_opacity': number + // 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 + // redux/selectionStore/isRemeberWinSize + 'feature.selection.is_remeber_win_size': boolean + // electronStore/SelectionAssistantRemeberWinSize/SelectionAssistantRemeberWinSize + 'feature.selection.remember_win_size': unknown | null + // redux/selectionStore/triggerMode + 'feature.selection.trigger_mode': string + // redux/settings/enableTopicNaming + 'topic.naming.enabled': boolean + // redux/settings/topicNamingPrompt + 'topic.naming.prompt': string + // redux/settings/pinTopicsToTop + 'topic.pin_to_top': boolean + // redux/settings/topicPosition + 'topic.position': string + // redux/settings/showTopicTime + 'topic.show_time': boolean + // redux/settings/assistantIconType + 'ui.assistant_icon_type': string + // redux/settings/clickAssistantToShowTopic + 'ui.click_assistant_to_show_topic': boolean + // redux/settings/customCss + 'ui.custom_css': string + // redux/settings/navbarPosition + 'ui.navbar.position': string } } /* eslint sort-keys: ["error", "asc", {"caseSensitive": true, "natural": false}] */ export const defaultPreferences: PreferencesType = { default: { - 'app.test5': { - content1: 'test5-1', - content2: 2 - }, - 'sys.a.test3': true, - 'sys.a.test4': ['test4-1', 'test4-2'], - 'ui.a.test1': 'test1', - 'ui.b.test2': 1 + 'app.developer_mode.enabled': false, + 'app.disable_hardware_acceleration': false, + 'app.dist.auto_update.enabled': true, + 'app.dist.test_plan.channel': 'UpgradeChannel.LATEST', + 'app.dist.test_plan.enabled': false, + 'app.language': null, + 'app.launch_on_boot': false, + 'app.notification.assistant.enabled': false, + 'app.notification.backup.enabled': false, + 'app.notification.knowledge.enabled': false, + 'app.privacy.data_collection.enabled': false, + 'app.proxy.bypass_rules': null, + 'app.proxy.mode': 'system', + 'app.proxy.url': null, + 'app.spell_check.enabled': false, + 'app.spell_check.languages': [], + 'app.theme.mode': 'ThemeMode.system', + 'app.theme.user_defined': { colorPrimary: '#00b96b' }, + 'app.theme.window_style': 'opaque', + 'app.tray.enabled': true, + 'app.tray.on_close': true, + 'app.tray.on_launch': false, + 'app.user.id': 'uuid()', + 'app.user.name': '', + 'app.zoom_factor': null, + 'chat.code.collapsible': false, + 'chat.code.editor.autocompletion': true, + 'chat.code.editor.fold_gutter': false, + 'chat.code.editor.highlight_active_line': false, + 'chat.code.editor.keymap': false, + 'chat.code.editor.theme_dark': 'auto', + 'chat.code.editor.theme_light': 'auto', + 'chat.code.execution.enabled': false, + 'chat.code.execution.timeout_minutes': 1, + 'chat.code.image_tools': false, + 'chat.code.preview.theme_dark': 'auto', + 'chat.code.preview.theme_light': 'auto', + 'chat.code.show_line_numbers': false, + 'chat.code.viewer.theme_dark': 'auto', + 'chat.code.viewer.theme_light': 'auto', + 'chat.code.wrappable': false, + 'chat.input.backspace_delete_model': true, + 'chat.input.paste_long_text_as_file': false, + 'chat.input.paste_long_text_threshold': 1500, + 'chat.input.quick_panel.triggers_enabled': false, + 'chat.input.send_message_shortcut': 'Enter', + 'chat.input.show_estimated_tokens': false, + 'chat.input.translate.auto_translate_with_space': false, + 'chat.input.translate.show_confirm': true, + 'chat.message.font': 'system', + 'chat.message.font_size': 14, + 'chat.message.math_engine': 'KaTeX', + 'chat.message.multi_model.fold_display_mode': 'expanded', + 'chat.message.multi_model.grid_columns': 2, + 'chat.message.multi_model.grid_popover_trigger': 'click', + 'chat.message.multi_model.style': 'horizontal', + 'chat.message.navigation_mode': 'none', + 'chat.message.show_divider': true, + 'chat.message.show_prompt': true, + 'chat.message.show_tokens': true, + 'chat.message.style': 'plain', + 'chat.message.thought.auto_collapse': true, + 'chat.narrow_mode': false, + 'data.backup.general.skip_backup_file': false, + 'data.backup.local.auto_sync': false, + 'data.backup.local.dir': '', + 'data.backup.local.max_backups': 0, + 'data.backup.local.skip_backup_file': false, + 'data.backup.local.sync_interval': 0, + 'data.backup.nutstore.auto_sync': null, + 'data.backup.nutstore.path': null, + 'data.backup.nutstore.skip_backup_file': null, + 'data.backup.nutstore.sync_interval': null, + 'data.backup.nutstore.sync_state': null, + 'data.backup.nutstore.token': null, + 'data.backup.s3.access_key_id': '', + 'data.backup.s3.auto_sync': false, + 'data.backup.s3.bucket': '', + 'data.backup.s3.endpoint': '', + 'data.backup.s3.max_backups': 0, + 'data.backup.s3.region': '', + 'data.backup.s3.root': '', + 'data.backup.s3.secret_access_key': '', + 'data.backup.s3.skip_backup_file': false, + 'data.backup.s3.sync_interval': 0, + 'data.backup.webdav.auto_sync': false, + 'data.backup.webdav.disable_stream': false, + 'data.backup.webdav.host': '', + 'data.backup.webdav.max_backups': 0, + 'data.backup.webdav.pass': '', + 'data.backup.webdav.path': '/cherry-studio', + 'data.backup.webdav.skip_backup_file': false, + 'data.backup.webdav.sync_interval': 0, + 'data.backup.webdav.user': '', + 'data.export.markdown.exclude_citations': false, + 'data.export.markdown.force_dollar_math': false, + 'data.export.markdown.path': null, + 'data.export.markdown.show_model_name': false, + 'data.export.markdown.show_model_provider': false, + 'data.export.markdown.standardize_citations': false, + 'data.export.markdown.use_topic_naming_for_message_title': false, + 'data.export.menus.docx': true, + 'data.export.menus.image': true, + 'data.export.menus.joplin': true, + 'data.export.menus.markdown': true, + 'data.export.menus.markdown_reason': true, + 'data.export.menus.notion': true, + 'data.export.menus.obsidian': true, + 'data.export.menus.plain_text': true, + 'data.export.menus.siyuan': true, + 'data.export.menus.yuque': true, + 'data.integration.joplin.export_reasoning': false, + 'data.integration.joplin.token': '', + 'data.integration.joplin.url': '', + 'data.integration.notion.api_key': '', + 'data.integration.notion.database_id': '', + 'data.integration.notion.export_reasoning': false, + 'data.integration.notion.page_name_key': 'Name', + 'data.integration.obsidian.default_vault': null, + 'data.integration.siyuan.api_url': null, + 'data.integration.siyuan.box_id': null, + 'data.integration.siyuan.root_path': null, + 'data.integration.siyuan.token': null, + 'data.integration.yuque.repo_id': '', + 'data.integration.yuque.token': '', + 'data.integration.yuque.url': '', + 'feature.csaas.api_key': '`cs-sk-${uuid()}`', + 'feature.csaas.enabled': false, + 'feature.csaas.host': 'localhost', + 'feature.csaas.port': 23333, + 'feature.minapp.max_keep_alive': 3, + 'feature.minapp.open_link_external': false, + 'feature.minapp.show_opened_in_sidebar': true, + 'feature.quick_assistant.click_tray_to_show': false, + 'feature.quick_assistant.enabled': false, + 'feature.quick_assistant.read_clipboard_at_startup': true, + 'feature.selection.action_items': [], + 'feature.selection.action_window_opacity': 100, + '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.trigger_mode': 'selected', + 'topic.naming.enabled': true, + 'topic.naming.prompt': '', + 'topic.pin_to_top': false, + 'topic.position': 'left', + 'topic.show_time': false, + 'ui.assistant_icon_type': 'emoji', + 'ui.click_assistant_to_show_topic': true, + 'ui.custom_css': '', + 'ui.navbar.position': 'top' } } + +// === AUTO-GENERATED CONTENT END === + +/** + * 生成统计: + * - 总配置项: 158 + * - electronStore项: 4 + * - redux项: 154 + * - localStorage项: 0 + */ diff --git a/src/main/data/migrate/electronStoreToPreferences.ts b/src/main/data/migrate/electronStoreToPreferences.ts new file mode 100644 index 0000000000..40f076421a --- /dev/null +++ b/src/main/data/migrate/electronStoreToPreferences.ts @@ -0,0 +1,152 @@ +/** + * Auto-generated ElectronStore to Preferences migration + * Generated at: 2025-08-09T07:20:05.910Z + * + * === AUTO-GENERATED CONTENT START === + */ + +import dbService from '@data/db/DbService' +import { loggerService } from '@logger' +import { configManager } from '@main/services/ConfigManager' + +import type { MigrationResult } from './index' +import { TypeConverter } from './utils/typeConverters' + +const logger = loggerService.withContext('ElectronStoreMigrator') + +// 键映射表 +const KEY_MAPPINGS = [ + { + originalKey: 'Language', + targetKey: 'app.language', + sourceCategory: 'Language', + type: 'unknown', + defaultValue: null + }, + { + originalKey: 'SelectionAssistantFollowToolbar', + targetKey: 'feature.selection.follow_toolbar', + sourceCategory: 'SelectionAssistantFollowToolbar', + type: 'unknown', + defaultValue: null + }, + { + originalKey: 'SelectionAssistantRemeberWinSize', + targetKey: 'feature.selection.remember_win_size', + sourceCategory: 'SelectionAssistantRemeberWinSize', + type: 'unknown', + defaultValue: null + }, + { + originalKey: 'ZoomFactor', + targetKey: 'app.zoom_factor', + sourceCategory: 'ZoomFactor', + type: 'unknown', + defaultValue: null + } +] as const + +export class ElectronStoreMigrator { + private typeConverter: TypeConverter + + constructor() { + this.typeConverter = new TypeConverter() + } + + /** + * 执行ElectronStore到preferences的迁移 + */ + async migrate(): Promise { + logger.info('开始ElectronStore迁移', { totalItems: KEY_MAPPINGS.length }) + + const result: MigrationResult = { + success: true, + migratedCount: 0, + errors: [], + source: 'electronStore' + } + + for (const mapping of KEY_MAPPINGS) { + try { + await this.migrateItem(mapping) + result.migratedCount++ + } catch (error) { + logger.error('迁移单项失败', { mapping, error }) + result.errors.push({ + key: mapping.originalKey, + error: error instanceof Error ? error.message : String(error) + }) + result.success = false + } + } + + logger.info('ElectronStore迁移完成', result) + return result + } + + /** + * 迁移单个配置项 + */ + private async migrateItem(mapping: (typeof KEY_MAPPINGS)[0]): Promise { + const { originalKey, targetKey, type, defaultValue } = mapping + + // 从ElectronStore读取原始值 + const originalValue = configManager.get(originalKey) + + if (originalValue === undefined || originalValue === null) { + // 如果原始值不存在,使用默认值 + if (defaultValue !== null && defaultValue !== undefined) { + const convertedValue = this.typeConverter.convert(defaultValue, type) + await dbService.setPreference('default', targetKey, convertedValue) + logger.debug('使用默认值迁移', { originalKey, targetKey, defaultValue: convertedValue }) + } + return + } + + // 类型转换 + const convertedValue = this.typeConverter.convert(originalValue, type) + + // 写入preferences表 + await dbService.setPreference('default', targetKey, convertedValue) + + logger.debug('成功迁移配置项', { + originalKey, + targetKey, + originalValue, + convertedValue + }) + } + + /** + * 验证迁移结果 + */ + async validateMigration(): Promise { + logger.info('开始验证ElectronStore迁移结果') + + for (const mapping of KEY_MAPPINGS) { + const { targetKey } = mapping + + try { + const value = await dbService.getPreference('default', targetKey) + if (value === null) { + logger.error('验证失败:配置项不存在', { targetKey }) + return false + } + } catch (error) { + logger.error('验证失败:读取配置项错误', { targetKey, error }) + return false + } + } + + logger.info('ElectronStore迁移验证成功') + return true + } +} + +// === AUTO-GENERATED CONTENT END === + +/** + * 迁移统计: + * - ElectronStore配置项: 4 + * - 包含的原始键: Language, SelectionAssistantFollowToolbar, SelectionAssistantRemeberWinSize, ZoomFactor + */ diff --git a/src/main/data/migrate/index.ts b/src/main/data/migrate/index.ts new file mode 100644 index 0000000000..45e34375ac --- /dev/null +++ b/src/main/data/migrate/index.ts @@ -0,0 +1,132 @@ +/** + * Auto-generated migration index + * Generated at: 2025-08-09T07:20:05.909Z + * + * This file is automatically generated from classification.json + * To update this file, modify classification.json and run: + * node .claude/data-classify/scripts/generate-migration.js + * + * === AUTO-GENERATED CONTENT START === + */ + +import { ElectronStoreMigrator } from './electronStoreToPreferences' +import { ReduxMigrator } from './reduxToPreferences' +import { loggerService } from '@logger' + +const logger = loggerService.withContext('MigrationManager') + +export interface MigrationResult { + success: boolean + migratedCount: number + errors: Array<{ + key: string + error: string + }> + source: 'electronStore' | 'redux' +} + +export interface MigrationSummary { + totalItems: number + successCount: number + errorCount: number + electronStore: MigrationResult + redux: MigrationResult +} + +export class MigrationManager { + private electronStoreMigrator: ElectronStoreMigrator + private reduxMigrator: ReduxMigrator + + constructor() { + this.electronStoreMigrator = new ElectronStoreMigrator() + this.reduxMigrator = new ReduxMigrator() + } + + /** + * 执行完整的preferences迁移 + * @returns 迁移摘要 + */ + async migrateAllPreferences(): Promise { + logger.info('开始完整preferences迁移') + + try { + // 并行执行两个迁移器 + const [electronStoreResult, reduxResult] = await Promise.all([ + this.electronStoreMigrator.migrate(), + this.reduxMigrator.migrate() + ]) + + const summary: MigrationSummary = { + totalItems: 158, + successCount: electronStoreResult.migratedCount + reduxResult.migratedCount, + errorCount: electronStoreResult.errors.length + reduxResult.errors.length, + electronStore: electronStoreResult, + redux: reduxResult + } + + if (summary.errorCount > 0) { + logger.warn('迁移完成但有错误', { summary }) + } else { + logger.info('迁移完全成功', { summary }) + } + + return summary + } catch (error) { + logger.error('迁移过程中发生致命错误', error) + throw error + } + } + + /** + * 验证迁移结果 + * @param summary 迁移摘要 + * @returns 是否验证成功 + */ + async validateMigration(summary: MigrationSummary): Promise { + logger.info('开始验证迁移结果') + + // 基本验证:检查成功率 + const successRate = summary.successCount / summary.totalItems + if (successRate < 0.95) { // 要求95%以上成功率 + logger.error('迁移成功率过低', { successRate, summary }) + return false + } + + // 验证关键配置项是否存在 + const criticalKeys = [ + 'app.theme.mode', + 'app.language', + 'app.user.id', + 'feature.quick_assistant.enabled', + 'chat.message.font_size' + ] + + try { + const dbServiceModule = await import('@main/db/DbService') + const dbService = dbServiceModule.default + + for (const key of criticalKeys) { + const result = await dbService.getPreference('default', key) + if (result === null) { + logger.error('关键配置项迁移失败', { key }) + return false + } + } + + logger.info('迁移验证成功') + return true + } catch (error) { + logger.error('验证过程中发生错误', error) + return false + } + } +} + +// === AUTO-GENERATED CONTENT END === + +/** + * 生成统计: + * - 总迁移项: 158 + * - ElectronStore项: 4 + * - Redux项: 154 + */ \ No newline at end of file diff --git a/src/main/data/migrate/reduxToPreferences.ts b/src/main/data/migrate/reduxToPreferences.ts new file mode 100644 index 0000000000..4e7adf46cd --- /dev/null +++ b/src/main/data/migrate/reduxToPreferences.ts @@ -0,0 +1,1081 @@ +/** + * Auto-generated Redux Store to Preferences migration + * Generated at: 2025-08-09T07:20:05.911Z + * + * === AUTO-GENERATED CONTENT START === + */ + +import dbService from '@data/db/DbService' +import { loggerService } from '@logger' + +import type { MigrationResult } from './index' +import { TypeConverter } from './utils/typeConverters' + +const logger = loggerService.withContext('ReduxMigrator') + +// Redux Store键映射表,按category分组 +const REDUX_MAPPINGS = [ + { + category: 'settings', + items: [ + { + originalKey: 'autoCheckUpdate', + targetKey: 'app.dist.auto_update.enabled', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'clickTrayToShowQuickAssistant', + targetKey: 'feature.quick_assistant.click_tray_to_show', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'disableHardwareAcceleration', + targetKey: 'app.disable_hardware_acceleration', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'enableDataCollection', + targetKey: 'app.privacy.data_collection.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'enableDeveloperMode', + targetKey: 'app.developer_mode.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'enableQuickAssistant', + targetKey: 'feature.quick_assistant.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'launchToTray', + targetKey: 'app.tray.on_launch', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'testChannel', + targetKey: 'app.dist.test_plan.channel', + type: 'string', + defaultValue: 'UpgradeChannel.LATEST' + }, + { + originalKey: 'testPlan', + targetKey: 'app.dist.test_plan.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'theme', + targetKey: 'app.theme.mode', + type: 'string', + defaultValue: 'ThemeMode.system' + }, + { + originalKey: 'tray', + targetKey: 'app.tray.enabled', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'trayOnClose', + targetKey: 'app.tray.on_close', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'sendMessageShortcut', + targetKey: 'chat.input.send_message_shortcut', + type: 'string', + defaultValue: 'Enter' + }, + { + originalKey: 'proxyMode', + targetKey: 'app.proxy.mode', + type: 'string', + defaultValue: 'system' + }, + { + originalKey: 'proxyUrl', + targetKey: 'app.proxy.url', + type: 'string' + }, + { + originalKey: 'proxyBypassRules', + targetKey: 'app.proxy.bypass_rules', + type: 'string' + }, + { + originalKey: 'userName', + targetKey: 'app.user.name', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'userId', + targetKey: 'app.user.id', + type: 'string', + defaultValue: 'uuid()' + }, + { + originalKey: 'showPrompt', + targetKey: 'chat.message.show_prompt', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'showTokens', + targetKey: 'chat.message.show_tokens', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'showMessageDivider', + targetKey: 'chat.message.show_divider', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'messageFont', + targetKey: 'chat.message.font', + type: 'string', + defaultValue: 'system' + }, + { + originalKey: 'showInputEstimatedTokens', + targetKey: 'chat.input.show_estimated_tokens', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'launchOnBoot', + targetKey: 'app.launch_on_boot', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'userTheme', + targetKey: 'app.theme.user_defined', + type: 'object', + defaultValue: { + colorPrimary: '#00b96b' + } + }, + { + originalKey: 'windowStyle', + targetKey: 'app.theme.window_style', + type: 'string', + defaultValue: 'opaque' + }, + { + originalKey: 'fontSize', + targetKey: 'chat.message.font_size', + type: 'number', + defaultValue: 14 + }, + { + originalKey: 'topicPosition', + targetKey: 'topic.position', + type: 'string', + defaultValue: 'left' + }, + { + originalKey: 'showTopicTime', + targetKey: 'topic.show_time', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'pinTopicsToTop', + targetKey: 'topic.pin_to_top', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'assistantIconType', + targetKey: 'ui.assistant_icon_type', + type: 'string', + defaultValue: 'emoji' + }, + { + originalKey: 'pasteLongTextAsFile', + targetKey: 'chat.input.paste_long_text_as_file', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'pasteLongTextThreshold', + targetKey: 'chat.input.paste_long_text_threshold', + type: 'number', + defaultValue: 1500 + }, + { + originalKey: 'clickAssistantToShowTopic', + targetKey: 'ui.click_assistant_to_show_topic', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'codeExecution.enabled', + targetKey: 'chat.code.execution.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codeExecution.timeoutMinutes', + targetKey: 'chat.code.execution.timeout_minutes', + type: 'number', + defaultValue: 1 + }, + { + originalKey: 'codeEditor.enabled', + targetKey: 'chat.code.editor.highlight_active_line', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codeEditor.themeLight', + targetKey: 'chat.code.editor.theme_light', + type: 'string', + defaultValue: 'auto' + }, + { + originalKey: 'codeEditor.themeDark', + targetKey: 'chat.code.editor.theme_dark', + type: 'string', + defaultValue: 'auto' + }, + { + originalKey: 'codeEditor.foldGutter', + targetKey: 'chat.code.editor.fold_gutter', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codeEditor.autocompletion', + targetKey: 'chat.code.editor.autocompletion', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'codeEditor.keymap', + targetKey: 'chat.code.editor.keymap', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codePreview.themeLight', + targetKey: 'chat.code.preview.theme_light', + type: 'string', + defaultValue: 'auto' + }, + { + originalKey: 'codePreview.themeDark', + targetKey: 'chat.code.preview.theme_dark', + type: 'string', + defaultValue: 'auto' + }, + { + originalKey: 'codeViewer.themeLight', + targetKey: 'chat.code.viewer.theme_light', + type: 'string', + defaultValue: 'auto' + }, + { + originalKey: 'codeViewer.themeDark', + targetKey: 'chat.code.viewer.theme_dark', + type: 'string', + defaultValue: 'auto' + }, + { + originalKey: 'codeShowLineNumbers', + targetKey: 'chat.code.show_line_numbers', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codeCollapsible', + targetKey: 'chat.code.collapsible', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codeWrappable', + targetKey: 'chat.code.wrappable', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'codeImageTools', + targetKey: 'chat.code.image_tools', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'mathEngine', + targetKey: 'chat.message.math_engine', + type: 'string', + defaultValue: 'KaTeX' + }, + { + originalKey: 'messageStyle', + targetKey: 'chat.message.style', + type: 'string', + defaultValue: 'plain' + }, + { + originalKey: 'foldDisplayMode', + targetKey: 'chat.message.multi_model.fold_display_mode', + type: 'string', + defaultValue: 'expanded' + }, + { + originalKey: 'gridColumns', + targetKey: 'chat.message.multi_model.grid_columns', + type: 'number', + defaultValue: 2 + }, + { + originalKey: 'gridPopoverTrigger', + targetKey: 'chat.message.multi_model.grid_popover_trigger', + type: 'string', + defaultValue: 'click' + }, + { + originalKey: 'messageNavigation', + targetKey: 'chat.message.navigation_mode', + type: 'string', + defaultValue: 'none' + }, + { + originalKey: 'skipBackupFile', + targetKey: 'data.backup.general.skip_backup_file', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'webdavHost', + targetKey: 'data.backup.webdav.host', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'webdavUser', + targetKey: 'data.backup.webdav.user', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'webdavPass', + targetKey: 'data.backup.webdav.pass', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'webdavPath', + targetKey: 'data.backup.webdav.path', + type: 'string', + defaultValue: '/cherry-studio' + }, + { + originalKey: 'webdavAutoSync', + targetKey: 'data.backup.webdav.auto_sync', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'webdavSyncInterval', + targetKey: 'data.backup.webdav.sync_interval', + type: 'number', + defaultValue: 0 + }, + { + originalKey: 'webdavMaxBackups', + targetKey: 'data.backup.webdav.max_backups', + type: 'number', + defaultValue: 0 + }, + { + originalKey: 'webdavSkipBackupFile', + targetKey: 'data.backup.webdav.skip_backup_file', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'webdavDisableStream', + targetKey: 'data.backup.webdav.disable_stream', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'autoTranslateWithSpace', + targetKey: 'chat.input.translate.auto_translate_with_space', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'showTranslateConfirm', + targetKey: 'chat.input.translate.show_confirm', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'enableTopicNaming', + targetKey: 'topic.naming.enabled', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'customCss', + targetKey: 'ui.custom_css', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'topicNamingPrompt', + targetKey: 'topic.naming.prompt', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'narrowMode', + targetKey: 'chat.narrow_mode', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'multiModelMessageStyle', + targetKey: 'chat.message.multi_model.style', + type: 'string', + defaultValue: 'horizontal' + }, + { + originalKey: 'readClipboardAtStartup', + targetKey: 'feature.quick_assistant.read_clipboard_at_startup', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'notionDatabaseID', + targetKey: 'data.integration.notion.database_id', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'notionApiKey', + targetKey: 'data.integration.notion.api_key', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'notionPageNameKey', + targetKey: 'data.integration.notion.page_name_key', + type: 'string', + defaultValue: 'Name' + }, + { + originalKey: 'markdownExportPath', + targetKey: 'data.export.markdown.path', + type: 'string', + defaultValue: null + }, + { + originalKey: 'forceDollarMathInMarkdown', + targetKey: 'data.export.markdown.force_dollar_math', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'useTopicNamingForMessageTitle', + targetKey: 'data.export.markdown.use_topic_naming_for_message_title', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'showModelNameInMarkdown', + targetKey: 'data.export.markdown.show_model_name', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'showModelProviderInMarkdown', + targetKey: 'data.export.markdown.show_model_provider', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'thoughtAutoCollapse', + targetKey: 'chat.message.thought.auto_collapse', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'notionExportReasoning', + targetKey: 'data.integration.notion.export_reasoning', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'excludeCitationsInExport', + targetKey: 'data.export.markdown.exclude_citations', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'standardizeCitationsInExport', + targetKey: 'data.export.markdown.standardize_citations', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'yuqueToken', + targetKey: 'data.integration.yuque.token', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'yuqueUrl', + targetKey: 'data.integration.yuque.url', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'yuqueRepoId', + targetKey: 'data.integration.yuque.repo_id', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'joplinToken', + targetKey: 'data.integration.joplin.token', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'joplinUrl', + targetKey: 'data.integration.joplin.url', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'joplinExportReasoning', + targetKey: 'data.integration.joplin.export_reasoning', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'defaultObsidianVault', + targetKey: 'data.integration.obsidian.default_vault', + type: 'string', + defaultValue: null + }, + { + originalKey: 'siyuanApiUrl', + targetKey: 'data.integration.siyuan.api_url', + type: 'string', + defaultValue: null + }, + { + originalKey: 'siyuanToken', + targetKey: 'data.integration.siyuan.token', + type: 'string', + defaultValue: null + }, + { + originalKey: 'siyuanBoxId', + targetKey: 'data.integration.siyuan.box_id', + type: 'string', + defaultValue: null + }, + { + originalKey: 'siyuanRootPath', + targetKey: 'data.integration.siyuan.root_path', + type: 'string', + defaultValue: null + }, + { + originalKey: 'maxKeepAliveMinapps', + targetKey: 'feature.minapp.max_keep_alive', + type: 'number', + defaultValue: 3 + }, + { + originalKey: 'showOpenedMinappsInSidebar', + targetKey: 'feature.minapp.show_opened_in_sidebar', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'minappsOpenLinkExternal', + targetKey: 'feature.minapp.open_link_external', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'enableSpellCheck', + targetKey: 'app.spell_check.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'spellCheckLanguages', + targetKey: 'app.spell_check.languages', + type: 'array', + defaultValue: [] + }, + { + originalKey: 'enableQuickPanelTriggers', + targetKey: 'chat.input.quick_panel.triggers_enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'enableBackspaceDeleteModel', + targetKey: 'chat.input.backspace_delete_model', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.image', + targetKey: 'data.export.menus.image', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.markdown', + targetKey: 'data.export.menus.markdown', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.markdown_reason', + targetKey: 'data.export.menus.markdown_reason', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.notion', + targetKey: 'data.export.menus.notion', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.yuque', + targetKey: 'data.export.menus.yuque', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.joplin', + targetKey: 'data.export.menus.joplin', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.obsidian', + targetKey: 'data.export.menus.obsidian', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.siyuan', + targetKey: 'data.export.menus.siyuan', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.docx', + targetKey: 'data.export.menus.docx', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'exportMenuOptions.plain_text', + targetKey: 'data.export.menus.plain_text', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'notification.assistant', + targetKey: 'app.notification.assistant.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'notification.backup', + targetKey: 'app.notification.backup.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'notification.knowledge', + targetKey: 'app.notification.knowledge.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'localBackupDir', + targetKey: 'data.backup.local.dir', + type: 'string', + defaultValue: '' + }, + { + originalKey: 'localBackupAutoSync', + targetKey: 'data.backup.local.auto_sync', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'localBackupSyncInterval', + targetKey: 'data.backup.local.sync_interval', + type: 'number', + defaultValue: 0 + }, + { + originalKey: 'localBackupMaxBackups', + targetKey: 'data.backup.local.max_backups', + type: 'number', + defaultValue: 0 + }, + { + originalKey: 'localBackupSkipBackupFile', + targetKey: 'data.backup.local.skip_backup_file', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 's3.endpoint', + targetKey: 'data.backup.s3.endpoint', + type: 'string', + defaultValue: '' + }, + { + originalKey: 's3.region', + targetKey: 'data.backup.s3.region', + type: 'string', + defaultValue: '' + }, + { + originalKey: 's3.bucket', + targetKey: 'data.backup.s3.bucket', + type: 'string', + defaultValue: '' + }, + { + originalKey: 's3.accessKeyId', + targetKey: 'data.backup.s3.access_key_id', + type: 'string', + defaultValue: '' + }, + { + originalKey: 's3.secretAccessKey', + targetKey: 'data.backup.s3.secret_access_key', + type: 'string', + defaultValue: '' + }, + { + originalKey: 's3.root', + targetKey: 'data.backup.s3.root', + type: 'string', + defaultValue: '' + }, + { + originalKey: 's3.autoSync', + targetKey: 'data.backup.s3.auto_sync', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 's3.syncInterval', + targetKey: 'data.backup.s3.sync_interval', + type: 'number', + defaultValue: 0 + }, + { + originalKey: 's3.maxBackups', + targetKey: 'data.backup.s3.max_backups', + type: 'number', + defaultValue: 0 + }, + { + originalKey: 's3.skipBackupFile', + targetKey: 'data.backup.s3.skip_backup_file', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'navbarPosition', + targetKey: 'ui.navbar.position', + type: 'string', + defaultValue: 'top' + }, + { + originalKey: 'apiServer.enabled', + targetKey: 'feature.csaas.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'apiServer.host', + targetKey: 'feature.csaas.host', + type: 'string', + defaultValue: 'localhost' + }, + { + originalKey: 'apiServer.port', + targetKey: 'feature.csaas.port', + type: 'number', + defaultValue: 23333 + }, + { + originalKey: 'apiServer.apiKey', + targetKey: 'feature.csaas.api_key', + type: 'string', + defaultValue: '`cs-sk-${uuid()}`' + } + ] + }, + { + category: 'selectionStore', + items: [ + { + originalKey: 'selectionEnabled', + targetKey: 'feature.selection.enabled', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'filterList', + targetKey: 'feature.selection.filter_list', + type: 'array', + defaultValue: [] + }, + { + originalKey: 'filterMode', + targetKey: 'feature.selection.filter_mode', + type: 'string', + defaultValue: 'default' + }, + { + originalKey: 'triggerMode', + targetKey: 'feature.selection.trigger_mode', + type: 'string', + defaultValue: 'selected' + }, + { + originalKey: 'isCompact', + targetKey: 'feature.selection.is_compact', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'isAutoClose', + targetKey: 'feature.selection.is_auto_close', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'isAutoPin', + targetKey: 'feature.selection.is_auto_pin', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'isFollowToolbar', + targetKey: 'feature.selection.is_follow_toolbar', + type: 'boolean', + defaultValue: true + }, + { + originalKey: 'isRemeberWinSize', + targetKey: 'feature.selection.is_remeber_win_size', + type: 'boolean', + defaultValue: false + }, + { + originalKey: 'actionWindowOpacity', + targetKey: 'feature.selection.action_window_opacity', + type: 'number', + defaultValue: 100 + }, + { + originalKey: 'actionItems', + targetKey: 'feature.selection.action_items', + type: 'array', + defaultValue: [] + } + ] + }, + { + category: 'nutstore', + items: [ + { + originalKey: 'nutstoreToken', + targetKey: 'data.backup.nutstore.token', + type: 'string', + defaultValue: null + }, + { + originalKey: 'nutstorePath', + targetKey: 'data.backup.nutstore.path', + type: 'string', + defaultValue: null + }, + { + originalKey: 'nutstoreAutoSync', + targetKey: 'data.backup.nutstore.auto_sync', + type: 'boolean', + defaultValue: null + }, + { + originalKey: 'nutstoreSyncInterval', + targetKey: 'data.backup.nutstore.sync_interval', + type: 'number', + defaultValue: null + }, + { + originalKey: 'nutstoreSyncState', + targetKey: 'data.backup.nutstore.sync_state', + type: 'object', + defaultValue: null + }, + { + originalKey: 'nutstoreSkipBackupFile', + targetKey: 'data.backup.nutstore.skip_backup_file', + type: 'boolean', + defaultValue: null + } + ] + } +] as const + +export class ReduxMigrator { + private typeConverter: TypeConverter + + constructor() { + this.typeConverter = new TypeConverter() + } + + /** + * 执行Redux Store到preferences的迁移 + */ + async migrate(): Promise { + const totalItems = REDUX_MAPPINGS.reduce((sum, group) => sum + group.items.length, 0) + logger.info('开始Redux Store迁移', { totalItems }) + + const result: MigrationResult = { + success: true, + migratedCount: 0, + errors: [], + source: 'redux' + } + + // 读取Redux持久化数据 + const persistedData = await this.loadPersistedReduxData() + + if (!persistedData) { + logger.warn('未找到Redis持久化数据,跳过迁移') + return result + } + + for (const categoryGroup of REDUX_MAPPINGS) { + const { category, items } = categoryGroup + + for (const mapping of items) { + try { + await this.migrateReduxItem(persistedData, category, mapping) + result.migratedCount++ + } catch (error) { + logger.error('迁移Redux项失败', { category, mapping, error }) + result.errors.push({ + key: `${category}.${mapping.originalKey}`, + error: error instanceof Error ? error.message : String(error) + }) + result.success = false + } + } + } + + logger.info('Redux Store迁移完成', result) + return result + } + + /** + * 从localStorage读取持久化的Redux数据 + */ + private async loadPersistedReduxData(): Promise { + try { + // 注意:这里需要在renderer进程中执行,或者通过IPC获取 + // 暂时返回null,实际实现需要根据项目架构调整 + logger.warn('loadPersistedReduxData需要具体实现') + return null + } catch (error) { + logger.error('读取Redux持久化数据失败', error as Error) + return null + } + } + + /** + * 迁移单个Redux配置项 + */ + private async migrateReduxItem( + persistedData: any, + category: string, + mapping: (typeof REDUX_MAPPINGS)[0]['items'][0] + ): Promise { + const { originalKey, targetKey, type, defaultValue } = mapping + + // 从持久化数据中提取原始值 + const categoryData = persistedData[category] + if (!categoryData) { + // 如果分类数据不存在,使用默认值 + if (defaultValue !== null && defaultValue !== undefined) { + const convertedValue = this.typeConverter.convert(defaultValue, type) + await dbService.setPreference('default', targetKey, convertedValue) + logger.debug('Redux分类不存在,使用默认值', { category, originalKey, targetKey, defaultValue }) + } + return + } + + const originalValue = categoryData[originalKey] + + if (originalValue === undefined || originalValue === null) { + // 如果原始值不存在,使用默认值 + if (defaultValue !== null && defaultValue !== undefined) { + const convertedValue = this.typeConverter.convert(defaultValue, type) + await dbService.setPreference('default', targetKey, convertedValue) + logger.debug('Redux值不存在,使用默认值', { category, originalKey, targetKey, defaultValue }) + } + return + } + + // 类型转换 + const convertedValue = this.typeConverter.convert(originalValue, type) + + // 写入preferences表 + await dbService.setPreference('default', targetKey, convertedValue) + + logger.debug('成功迁移Redux配置项', { + category, + originalKey, + targetKey, + originalValue, + convertedValue + }) + } +} + +// === AUTO-GENERATED CONTENT END === + +/** + * 迁移统计: + * - Redux配置项: 154 + * - 涉及的Redux分类: settings, selectionStore, nutstore + */ diff --git a/src/main/data/migrate/utils/migrationHelpers.ts b/src/main/data/migrate/utils/migrationHelpers.ts new file mode 100644 index 0000000000..c63143f71f --- /dev/null +++ b/src/main/data/migrate/utils/migrationHelpers.ts @@ -0,0 +1,72 @@ +/** + * 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/utils/typeConverters.ts b/src/main/data/migrate/utils/typeConverters.ts new file mode 100644 index 0000000000..f0c2e70b01 --- /dev/null +++ b/src/main/data/migrate/utils/typeConverters.ts @@ -0,0 +1,125 @@ +/** + * 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