diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 87ec8a7ce5..bbb8e2ecf8 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -134,7 +134,6 @@ export default defineConfig({ selectionToolbar: resolve(__dirname, 'src/renderer/selectionToolbar.html'), selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'), traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html'), - dataRefactorMigrate: resolve(__dirname, 'src/renderer/dataRefactorMigrate.html'), migrationV2: resolve(__dirname, 'src/renderer/migrationV2.html') }, onwarn(warning, warn) { diff --git a/eslint.config.mjs b/eslint.config.mjs index 7a443ed688..e72484a1d9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -140,7 +140,7 @@ export default defineConfig([ { // Component Rules - prevent importing antd components when migration completed files: ['**/*.{ts,tsx,js,jsx}'], - ignores: ['src/renderer/src/windows/dataRefactorTest/**/*.{ts,tsx}'], + ignores: [], rules: { // 'no-restricted-imports': [ // 'error', diff --git a/packages/shared/data/cache/cacheSchemas.ts b/packages/shared/data/cache/cacheSchemas.ts index 9be41126c0..f75fd4f4f3 100644 --- a/packages/shared/data/cache/cacheSchemas.ts +++ b/packages/shared/data/cache/cacheSchemas.ts @@ -26,20 +26,6 @@ export type UseCacheSchema = { 'topic.active': CacheValueTypes.CacheTopic | null 'topic.renaming': string[] 'topic.newly_renamed': string[] - - // Test keys (for dataRefactorTest window) - // TODO: remove after testing - 'test-hook-memory-1': string - 'test-ttl-cache': string - 'test-protected-cache': string - 'test-deep-equal': { nested: { count: number }; tags: string[] } - 'test-performance': number - 'test-multi-hook': string - 'concurrent-test-1': number - 'concurrent-test-2': number - 'large-data-test': Record - 'test-number-cache': number - 'test-object-cache': { name: string; count: number; active: boolean } } export const DefaultUseCache: UseCacheSchema = { @@ -70,21 +56,7 @@ export const DefaultUseCache: UseCacheSchema = { // Topic management 'topic.active': null, 'topic.renaming': [], - 'topic.newly_renamed': [], - - // Test keys (for dataRefactorTest window) - // TODO: remove after testing - 'test-hook-memory-1': 'default-memory-value', - 'test-ttl-cache': 'test-ttl-cache', - 'test-protected-cache': 'protected-value', - 'test-deep-equal': { nested: { count: 0 }, tags: ['initial'] }, - 'test-performance': 0, - 'test-multi-hook': 'hook-1-default', - 'concurrent-test-1': 0, - 'concurrent-test-2': 0, - 'large-data-test': {}, - 'test-number-cache': 42, - 'test-object-cache': { name: 'test', count: 0, active: true } + 'topic.newly_renamed': [] } /** @@ -92,22 +64,10 @@ export const DefaultUseCache: UseCacheSchema = { */ export type UseSharedCacheSchema = { 'example-key': string - - // Test keys (for dataRefactorTest window) - // TODO: remove after testing - 'test-hook-shared-1': string - 'test-multi-hook': string - 'concurrent-shared': number } export const DefaultUseSharedCache: UseSharedCacheSchema = { - 'example-key': 'example default value', - - // Test keys (for dataRefactorTest window) - // TODO: remove after testing - 'concurrent-shared': 0, - 'test-hook-shared-1': 'default-shared-value', - 'test-multi-hook': 'hook-3-shared' + 'example-key': 'example default value' } /** @@ -116,24 +76,10 @@ export const DefaultUseSharedCache: UseSharedCacheSchema = { */ export type RendererPersistCacheSchema = { 'example-key': string - - // Test keys (for dataRefactorTest window) - // TODO: remove after testing - 'example-1': string - 'example-2': string - 'example-3': string - 'example-4': string } export const DefaultRendererPersistCache: RendererPersistCacheSchema = { - 'example-key': 'example default value', - - // Test keys (for dataRefactorTest window) - // TODO: remove after testing - 'example-1': 'example default value', - 'example-2': 'example default value', - 'example-3': 'example default value', - 'example-4': 'example default value' + 'example-key': 'example default value' } /** diff --git a/src/main/data/README.md b/src/main/data/README.md index 30167d6537..7efff10113 100644 --- a/src/main/data/README.md +++ b/src/main/data/README.md @@ -30,7 +30,7 @@ src/main/data/ │ # - TopicRepository.ts # Complex: Topic data access │ # - MessageRepository.ts # Complex: Message data access │ -├── db/ # Database layer +├── db/ # Database layer │ ├── schemas/ # Drizzle table definitions │ │ ├── preference.ts # Preference configuration table │ │ ├── appState.ts # Application state table @@ -38,8 +38,8 @@ src/main/data/ │ ├── seeding/ # Database initialization │ └── DbService.ts # Database connection and management │ -├── migrate/ # Data migration system -│ └── dataRefactor/ # v2 data refactoring migration tools +├── migration/ # Data migration system +│ └── v2/ # v2 data refactoring migration tools │ ├── CacheService.ts # Infrastructure: Cache management ├── DataApiService.ts # Infrastructure: API coordination diff --git a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts b/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts deleted file mode 100644 index eb748b7667..0000000000 --- a/src/main/data/migrate/dataRefactor/DataRefactorMigrateService.ts +++ /dev/null @@ -1,983 +0,0 @@ -import { dbService } from '@data/db/DbService' -import { appStateTable } from '@data/db/schemas/appState' -import { loggerService } from '@logger' -import { isDev } from '@main/constant' -import BackupManager from '@main/services/BackupManager' -import { IpcChannel } from '@shared/IpcChannel' -import { eq } from 'drizzle-orm' -import { app, BrowserWindow, ipcMain } from 'electron' -import { app as electronApp } from 'electron' -import { join } from 'path' - -import { PreferencesMigrator } from './migrators/PreferencesMigrator' - -const logger = loggerService.withContext('DataRefactorMigrateService') - -const DATA_REFACTOR_MIGRATION_STATUS = 'data_refactor_migration_status' - -// Data refactor migration status interface -interface DataRefactorMigrationStatus { - completed: boolean - completedAt?: number - version?: string -} - -type MigrationStage = - | 'introduction' // Introduction phase - user can cancel - | 'backup_required' // Backup required - show backup requirement - | 'backup_progress' // Backup in progress - user is backing up - | 'backup_confirmed' // Backup confirmed - ready to migrate - | 'migration' // Migration in progress - cannot cancel - | 'completed' // Completed - restart app - | 'error' // Error - recovery options - -interface MigrationProgress { - stage: MigrationStage - progress: number - total: number - message: string - error?: string -} - -interface MigrationResult { - success: boolean - error?: string - migratedCount: number -} - -export class DataRefactorMigrateService { - private static instance: DataRefactorMigrateService | null = null - private migrateWindow: BrowserWindow | null = null - private testWindows: BrowserWindow[] = [] - private backupManager: BackupManager - private db = dbService.getDb() - private currentProgress: MigrationProgress = { - stage: 'introduction', - progress: 0, - total: 100, - message: 'Ready to start data migration' - } - private isMigrating: boolean = false - private reduxData: any = null // Cache for Redux persist data - - constructor() { - this.backupManager = new BackupManager() - } - - /** - * Get backup manager instance for integration with existing backup system - */ - public getBackupManager(): BackupManager { - 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 - */ - public registerMigrationIpcHandlers(): void { - logger.info('Registering migration-specific IPC handlers') - - // Only register the minimal IPC handlers needed for migration - ipcMain.handle(IpcChannel.DataMigrate_CheckNeeded, async () => { - try { - return await this.isMigrated() - } catch (error) { - logger.error('IPC handler error: checkMigrationNeeded', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_ProceedToBackup, async () => { - try { - await this.proceedToBackup() - return true - } catch (error) { - logger.error('IPC handler error: proceedToBackup', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_StartMigration, async () => { - try { - await this.startMigrationProcess() - return true - } catch (error) { - logger.error('IPC handler error: startMigrationProcess', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_RetryMigration, async () => { - try { - await this.retryMigration() - return true - } catch (error) { - logger.error('IPC handler error: retryMigration', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_GetProgress, () => { - try { - return this.getCurrentProgress() - } catch (error) { - logger.error('IPC handler error: getCurrentProgress', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_Cancel, async () => { - try { - return await this.cancelMigration() - } catch (error) { - logger.error('IPC handler error: cancelMigration', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, async () => { - try { - await this.notifyBackupCompleted() - return true - } catch (error) { - logger.error('IPC handler error: notifyBackupCompleted', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, async () => { - try { - logger.info('Opening backup dialog for migration') - - // Update progress to indicate backup dialog is opening - // await this.updateProgress('backup_progress', 10, 'Opening backup dialog...') - - // Instead of performing backup automatically, let's open the file dialog - // and let the user choose where to save the backup - const { dialog } = await import('electron') - const result = await dialog.showSaveDialog({ - title: 'Save Migration Backup', - defaultPath: `cherry-studio-migration-backup-${new Date().toISOString().split('T')[0]}.zip`, - filters: [ - { name: 'Backup Files', extensions: ['zip'] }, - { name: 'All Files', extensions: ['*'] } - ] - }) - - if (!result.canceled && result.filePath) { - logger.info('User selected backup location', { filePath: result.filePath }) - await this.updateProgress('backup_progress', 10, 'Creating backup file...') - - // Perform the actual backup to the selected location - const backupResult = await this.performBackupToFile(result.filePath) - - if (backupResult.success) { - await this.updateProgress('backup_progress', 100, 'Backup created successfully!') - // Wait a moment to show the success message, then transition to confirmed state - setTimeout(async () => { - await this.updateProgress( - 'backup_confirmed', - 100, - 'Backup completed! Ready to start migration. Click "Start Migration" to continue.' - ) - }, 1000) - } else { - await this.updateProgress('backup_required', 0, `Backup failed: ${backupResult.error}`) - } - - return backupResult - } else { - logger.info('User cancelled backup dialog') - await this.updateProgress('backup_required', 0, 'Backup cancelled. Please create a backup to continue.') - return { success: false, error: 'Backup cancelled by user' } - } - } catch (error) { - logger.error('IPC handler error: showBackupDialog', error as Error) - await this.updateProgress('backup_required', 0, 'Backup process failed') - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_StartFlow, async () => { - try { - return await this.startMigrationFlow() - } catch (error) { - logger.error('IPC handler error: startMigrationFlow', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_RestartApp, async () => { - try { - await this.restartApplication() - return true - } catch (error) { - logger.error('IPC handler error: restartApplication', error as Error) - throw error - } - }) - - ipcMain.handle(IpcChannel.DataMigrate_CloseWindow, () => { - try { - this.closeMigrateWindow() - return true - } catch (error) { - logger.error('IPC handler error: closeMigrateWindow', error as Error) - throw error - } - }) - - 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') - } - - /** - * Remove migration-specific IPC handlers - * Clean up when migration is complete or cancelled - */ - public unregisterMigrationIpcHandlers(): void { - logger.info('Unregistering migration-specific IPC handlers') - - try { - ipcMain.removeAllListeners(IpcChannel.DataMigrate_CheckNeeded) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_GetProgress) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_Cancel) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_BackupCompleted) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_ShowBackupDialog) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartFlow) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_ProceedToBackup) - ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration) - 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) { - logger.warn('Error unregistering migration IPC handlers', error as Error) - } - } - - public static getInstance(): DataRefactorMigrateService { - if (!DataRefactorMigrateService.instance) { - DataRefactorMigrateService.instance = new DataRefactorMigrateService() - } - return DataRefactorMigrateService.instance - } - - /** - * Convenient static method to open test window - */ - public static openTestWindow(): BrowserWindow { - const instance = DataRefactorMigrateService.getInstance() - return instance.createTestWindow() - } - - /** - * Check if migration is needed - */ - async isMigrated(): Promise { - try { - const isMigrated = await this.isMigrationCompleted() - if (isMigrated) { - logger.info('Data Refactor Migration already completed') - return true - } - - logger.info('Data Refactor Migration is needed') - return false - } catch (error) { - logger.error('Failed to check migration status', error as Error) - return false - } - } - - /** - * Check if migration is already completed - */ - private async isMigrationCompleted(): Promise { - try { - logger.debug('Checking migration completion status in database') - - // First check if the database is available - if (!this.db) { - logger.warn('Database not initialized, assuming migration not completed') - return false - } - - const result = await this.db - .select() - .from(appStateTable) - .where(eq(appStateTable.key, DATA_REFACTOR_MIGRATION_STATUS)) - .limit(1) - - logger.debug('Migration status query result', { resultCount: result.length }) - - if (result.length === 0) { - logger.info('No migration status record found, migration needed') - return false - } - - const status = result[0].value as DataRefactorMigrationStatus - const isCompleted = status.completed === true - - logger.info('Migration status found', { - completed: isCompleted, - completedAt: status.completedAt, - version: status.version - }) - - return isCompleted - } catch (error) { - logger.error('Failed to check migration state - treating as not completed', error as Error) - // In case of database errors, assume migration is needed to be safe - return false - } - } - - /** - * Mark migration as completed - */ - private async markMigrationCompleted(): Promise { - try { - const migrationStatus: DataRefactorMigrationStatus = { - completed: true, - completedAt: Date.now(), - version: electronApp.getVersion() - } - - await this.db - .insert(appStateTable) - .values({ - key: DATA_REFACTOR_MIGRATION_STATUS, - value: migrationStatus, // drizzle handles JSON serialization automatically - description: 'Data refactoring migration status from legacy format (ElectronStore + Redux persist) to SQLite', - createdAt: Date.now(), - updatedAt: Date.now() - }) - .onConflictDoUpdate({ - target: appStateTable.key, - set: { - value: migrationStatus, - updatedAt: Date.now() - } - }) - - logger.info('Migration marked as completed in app_state table', { - version: migrationStatus.version, - completedAt: migrationStatus.completedAt - }) - } catch (error) { - logger.error('Failed to mark migration as completed', error as Error) - throw error - } - } - - /** - * Create and show migration window - */ - private createMigrateWindow(): BrowserWindow { - if (this.migrateWindow && !this.migrateWindow.isDestroyed()) { - this.migrateWindow.show() - return this.migrateWindow - } - - // Register migration-specific IPC handlers before creating window - this.registerMigrationIpcHandlers() - - this.migrateWindow = new BrowserWindow({ - width: 640, - height: 480, - resizable: false, - maximizable: false, - minimizable: false, - show: false, - frame: false, - autoHideMenuBar: true, - webPreferences: { - preload: join(__dirname, '../preload/simplest.js'), - sandbox: false, - webSecurity: false, - contextIsolation: true - } - }) - - // Load the migration window - if (isDev && process.env['ELECTRON_RENDERER_URL']) { - this.migrateWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/dataRefactorMigrate.html') - } else { - this.migrateWindow.loadFile(join(__dirname, '../renderer/dataRefactorMigrate.html')) - } - - this.migrateWindow.once('ready-to-show', () => { - this.migrateWindow?.show() - }) - - this.migrateWindow.on('closed', () => { - this.migrateWindow = null - // Clean up IPC handlers when window is closed - this.unregisterMigrationIpcHandlers() - }) - - logger.info('Migration window created') - return this.migrateWindow - } - - /** - * Show migration window and initialize introduction stage - */ - async runMigration(): Promise { - if (this.isMigrating) { - logger.warn('Migration already in progress') - this.migrateWindow?.show() - return - } - - this.isMigrating = true - logger.info('Showing migration window') - - // Initialize introduction stage - await this.updateProgress('introduction', 0, 'Welcome to Cherry Studio data migration') - - // Create migration window - const window = this.createMigrateWindow() - - // Wait for window to be ready - await new Promise((resolve) => { - if (window.webContents.isLoading()) { - window.webContents.once('did-finish-load', () => resolve()) - } else { - resolve() - } - }) - } - - /** - * Start migration flow - simply ensure we're in introduction stage - * This is called when user first opens the migration window - */ - async startMigrationFlow(): Promise { - if (!this.isMigrating) { - logger.warn('Migration not started, cannot execute flow.') - return - } - - logger.info('Confirming introduction stage for migration flow') - await this.updateProgress('introduction', 0, 'Ready to begin migration process. Please read the information below.') - } - - /** - * Proceed from introduction to backup requirement stage - * This is called when user clicks "Next" in introduction - */ - async proceedToBackup(): Promise { - if (!this.isMigrating) { - logger.warn('Migration not started, cannot proceed to backup.') - return - } - - logger.info('Proceeding from introduction to backup stage') - await this.updateProgress('backup_required', 0, 'Data backup is required before migration can proceed') - } - - /** - * Start the actual migration process - * This is called when user confirms backup and clicks "Start Migration" - */ - async startMigrationProcess(): Promise { - if (!this.isMigrating) { - logger.warn('Migration not started, cannot start migration process.') - return - } - - logger.info('Starting actual migration process') - try { - await this.executeMigrationFlow() - } catch (error) { - logger.error('Migration process failed', error as Error) - // error is already handled in executeMigrationFlow - } - } - - /** - * Execute the actual migration process - * Called after user has confirmed backup completion - */ - private async executeMigrationFlow(): Promise { - try { - // Start migration - await this.updateProgress('migration', 0, 'Starting data migration...') - const migrationResult = await this.executeMigration() - - if (!migrationResult.success) { - throw new Error(migrationResult.error || 'Migration failed') - } - - await this.updateProgress( - 'migration', - 100, - `Migration completed: ${migrationResult.migratedCount} items migrated` - ) - - // Mark as completed - await this.markMigrationCompleted() - - await this.updateProgress('completed', 100, 'Migration completed successfully! Click restart to continue.') - } catch (error) { - logger.error('Migration flow failed', error as Error) - const errorMessage = error instanceof Error ? error.message : String(error) - await this.updateProgress( - 'error', - 0, - 'Migration failed. You can close this window and try again, or continue using the previous version.', - errorMessage - ) - - throw error - } - } - - /** - * Perform backup to a specific file location - */ - private async performBackupToFile(filePath: string): Promise<{ success: boolean; error?: string }> { - try { - logger.info('Performing backup to file', { filePath }) - - // Get backup data from the current application state - const backupData = await this.getBackupData() - - // Extract directory and filename from the full path - const path = await import('path') - const destinationDir = path.dirname(filePath) - const fileName = path.basename(filePath) - - // Use the existing backup manager to create a backup - const backupPath = await this.backupManager.backup( - null as any, // IpcMainInvokeEvent - we're calling directly so pass null - fileName, - backupData, - destinationDir, - false // Don't skip backup files - full backup for migration safety - ) - - if (backupPath) { - logger.info('Backup created successfully', { path: backupPath }) - return { success: true } - } else { - return { - success: false, - error: 'Backup process did not return a file path' - } - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error) - logger.error('Backup failed during migration:', error as Error) - return { - success: false, - error: errorMessage - } - } - } - - /** - * Get backup data from the current application - * This creates a minimal backup with essential system information - */ - private async getBackupData(): Promise { - try { - const fs = await import('fs-extra') - const path = await import('path') - - // Gather basic system information - const data = { - backup: { - timestamp: new Date().toISOString(), - version: electronApp.getVersion(), - type: 'pre-migration-backup', - note: 'This is a safety backup created before data migration' - }, - system: { - platform: process.platform, - arch: process.arch, - nodeVersion: process.version - }, - // Include basic configuration files if they exist - configs: {} as Record - } - - // Try to read some basic configuration files (non-critical if they fail) - try { - const { getDataPath } = await import('@main/utils') - const dataPath = getDataPath() - - // Check if there are any config files we should backup - const configFiles = ['config.json', 'settings.json', 'preferences.json'] - for (const configFile of configFiles) { - const configPath = path.join(dataPath, configFile) - if (await fs.pathExists(configPath)) { - try { - const configContent = await fs.readJson(configPath) - data.configs[configFile] = configContent - } catch (err) { - logger.warn(`Could not read config file ${configFile}`, err as Error) - } - } - } - } catch (err) { - logger.warn('Could not access data directory for config backup', err as Error) - } - - return JSON.stringify(data, null, 2) - } catch (error) { - logger.error('Failed to get backup data:', error as Error) - throw error - } - } - - /** - * Notify that backup has been completed (called from IPC handler) - */ - public async notifyBackupCompleted(): Promise { - logger.info('Backup completed by user') - await this.updateProgress( - 'backup_confirmed', - 100, - 'Backup completed! Ready to start migration. Click "Start Migration" to continue.' - ) - } - - /** - * Execute the actual migration - */ - private async executeMigration(): Promise { - try { - logger.info('Executing migration') - - // 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) => { - this.updateProgress('migration', progress, message) - }) - - logger.info('Migration execution completed', result) - - return { - success: result.success, - migratedCount: result.migratedCount, - error: result.errors.length > 0 ? result.errors.map((e) => e.error).join('; ') : undefined - } - } catch (error) { - logger.error('Migration execution failed', error as Error) - return { - success: false, - error: error instanceof Error ? error.message : String(error), - migratedCount: 0 - } - } - } - - /** - * Update migration progress and broadcast to window - */ - private async updateProgress( - stage: MigrationStage, - progress: number, - message: string, - error?: string - ): Promise { - this.currentProgress = { - stage, - progress, - total: 100, - message, - error - } - - if (this.migrateWindow && !this.migrateWindow.isDestroyed()) { - this.migrateWindow.webContents.send(IpcChannel.DataMigrateProgress, this.currentProgress) - } - - logger.debug('Progress updated', this.currentProgress) - } - - /** - * Get current migration progress - */ - getCurrentProgress(): MigrationProgress { - return this.currentProgress - } - - /** - * Cancel migration process - * Only allowed during introduction and backup phases - */ - async cancelMigration(): Promise { - if (!this.isMigrating) { - return - } - - const currentStage = this.currentProgress.stage - if (currentStage === 'migration') { - logger.warn('Cannot cancel migration during migration process') - return - } - - logger.info('Cancelling migration process') - this.isMigrating = false - this.closeMigrateWindow() - } - - /** - * Retry migration after error - */ - async retryMigration(): Promise { - logger.info('Retrying migration process') - await this.updateProgress( - 'introduction', - 0, - 'Ready to restart migration process. Please read the information below.' - ) - } - - /** - * Close migration window - */ - private closeMigrateWindow(): void { - if (this.migrateWindow && !this.migrateWindow.isDestroyed()) { - this.migrateWindow.close() - this.migrateWindow = null - } - - this.isMigrating = false - // Clean up migration-specific IPC handlers - this.unregisterMigrationIpcHandlers() - } - - /** - * Restart the application after successful migration - */ - private async restartApplication(): Promise { - try { - logger.info('Preparing to restart application after migration completion') - - // Ensure migration status is properly saved before restart - await this.verifyMigrationStatus() - - // Give some time for database operations to complete - await new Promise((resolve) => setTimeout(resolve, 500)) - - logger.info('Restarting application now') - - // In development mode, relaunch might not work properly - if (process.env.NODE_ENV === 'development' || !app.isPackaged) { - logger.warn('Development mode detected - showing restart instruction instead of auto-restart') - - const { dialog } = await import('electron') - await dialog.showMessageBox({ - type: 'info', - title: 'Migration Complete - Restart Required', - message: - 'Data migration completed successfully!\n\nSince you are in development mode, please manually restart the application to continue.', - buttons: ['Close App'], - defaultId: 0 - }) - - // Clean up migration window and handlers after showing dialog - this.closeMigrateWindow() - app.quit() - } else { - // Production mode - clean up first, then relaunch - this.closeMigrateWindow() - app.relaunch() - app.exit(0) - } - } catch (error) { - logger.error('Failed to restart application', error as Error) - // Update UI to show restart failure and provide manual restart instruction - await this.updateProgress( - 'error', - 0, - 'Application restart failed. Please manually restart the application to complete migration.', - error instanceof Error ? error.message : String(error) - ) - // Fallback: just close migration window and let user manually restart - this.closeMigrateWindow() - } - } - - /** - * Verify that migration status is properly saved - */ - private async verifyMigrationStatus(): Promise { - try { - const isCompleted = await this.isMigrationCompleted() - if (isCompleted) { - logger.info('Migration status verified as completed') - } else { - logger.warn('Migration status not found as completed, attempting to mark again') - await this.markMigrationCompleted() - - // Double-check - const recheck = await this.isMigrationCompleted() - if (recheck) { - logger.info('Migration status successfully marked as completed on retry') - } else { - logger.error('Failed to mark migration as completed even on retry') - } - } - } catch (error) { - logger.error('Failed to verify migration status', error as Error) - // Don't throw - still allow restart - } - } - - /** - * Create and show test window for testing PreferenceService and usePreference functionality - */ - public createTestWindow(): BrowserWindow { - const windowNumber = this.testWindows.length + 1 - - const testWindow = new BrowserWindow({ - width: 1000, - height: 700, - minWidth: 800, - minHeight: 600, - resizable: true, - maximizable: true, - minimizable: true, - show: false, - frame: true, - autoHideMenuBar: true, - title: `Data Refactor Test Window #${windowNumber} - PreferenceService Testing`, - webPreferences: { - preload: join(__dirname, '../preload/index.js'), - sandbox: false, - webSecurity: false, - contextIsolation: true - } - }) - - // Add to test windows array - this.testWindows.push(testWindow) - - // Load the test window - if (isDev && process.env['ELECTRON_RENDERER_URL']) { - testWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/dataRefactorTest.html') - // Open DevTools in development mode for easier testing - testWindow.webContents.openDevTools() - } else { - testWindow.loadFile(join(__dirname, '../renderer/dataRefactorTest.html')) - } - - testWindow.once('ready-to-show', () => { - testWindow?.show() - testWindow?.focus() - }) - - testWindow.on('closed', () => { - // Remove from test windows array when closed - const index = this.testWindows.indexOf(testWindow) - if (index > -1) { - this.testWindows.splice(index, 1) - } - }) - - logger.info(`Test window #${windowNumber} created for PreferenceService testing`) - return testWindow - } - - /** - * Get test window instance (first one) - */ - public getTestWindow(): BrowserWindow | null { - return this.testWindows.length > 0 ? this.testWindows[0] : null - } - - /** - * Get all test windows - */ - public getTestWindows(): BrowserWindow[] { - return this.testWindows.filter((window) => !window.isDestroyed()) - } - - /** - * Close all test windows - */ - public closeTestWindows(): void { - this.testWindows.forEach((window) => { - if (!window.isDestroyed()) { - window.close() - } - }) - this.testWindows = [] - logger.info('All test windows closed') - } - - /** - * Close a specific test window - */ - public closeTestWindow(window?: BrowserWindow): void { - if (window) { - if (!window.isDestroyed()) { - window.close() - } - } else { - // Close first window if no specific window provided - const firstWindow = this.getTestWindow() - if (firstWindow && !firstWindow.isDestroyed()) { - firstWindow.close() - } - } - } - - /** - * Check if any test windows are open - */ - public isTestWindowOpen(): boolean { - return this.testWindows.some((window) => !window.isDestroyed()) - } -} - -// Export singleton instance -export const dataRefactorMigrateService = DataRefactorMigrateService.getInstance() diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMappings.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMappings.ts deleted file mode 100644 index b3cfb07c6c..0000000000 --- a/src/main/data/migrate/dataRefactor/migrators/PreferencesMappings.ts +++ /dev/null @@ -1,755 +0,0 @@ -/** - * Auto-generated preference mappings from classification.json - * Generated at: 2025-09-02T06:27:50.213Z - * - * 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: '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: 'language', - targetKey: 'app.language' - }, - { - 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: 'ui.theme_mode' - }, - { - originalKey: 'tray', - targetKey: 'app.tray.enabled' - }, - { - originalKey: 'trayOnClose', - targetKey: 'app.tray.on_close' - }, - { - originalKey: 'showAssistants', - targetKey: 'assistant.tab.show' - }, - { - originalKey: 'showTopics', - targetKey: 'topic.tab.show' - }, - { - originalKey: 'assistantsTabSortType', - targetKey: 'assistant.tab.sort_type' - }, - { - originalKey: 'sendMessageShortcut', - targetKey: 'chat.input.send_message_shortcut' - }, - { - originalKey: 'targetLanguage', - targetKey: 'feature.translate.target_language' - }, - { - 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: '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.colorPrimary', - targetKey: 'ui.theme_user.color_primary' - }, - { - originalKey: 'windowStyle', - targetKey: 'ui.window_style' - }, - { - originalKey: 'fontSize', - targetKey: 'chat.message.font_size' - }, - { - originalKey: 'topicPosition', - targetKey: 'topic.position' - }, - { - originalKey: 'showTopicTime', - targetKey: 'topic.tab.show_time' - }, - { - originalKey: 'pinTopicsToTop', - targetKey: 'topic.tab.pin_to_top' - }, - { - originalKey: 'assistantIconType', - targetKey: '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: 'assistant.click_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.enabled' - }, - { - originalKey: 'codeEditor.themeLight', - targetKey: 'chat.code.editor.theme_light' - }, - { - originalKey: 'codeEditor.themeDark', - targetKey: 'chat.code.editor.theme_dark' - }, - { - originalKey: 'codeEditor.highlightActiveLine', - targetKey: 'chat.code.editor.highlight_active_line' - }, - { - 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: 'translateModelPrompt', - targetKey: 'feature.translate.model_prompt' - }, - { - 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: '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' - } - ], - shortcuts: [ - { - originalKey: 'shortcuts.zoom_in', - targetKey: 'shortcut.app.zoom_in' - }, - { - originalKey: 'shortcuts.zoom_out', - targetKey: 'shortcut.app.zoom_out' - }, - { - originalKey: 'shortcuts.zoom_reset', - targetKey: 'shortcut.app.zoom_reset' - }, - { - originalKey: 'shortcuts.show_settings', - targetKey: 'shortcut.app.show_settings' - }, - { - originalKey: 'shortcuts.show_app', - targetKey: 'shortcut.app.show_main_window' - }, - { - originalKey: 'shortcuts.mini_window', - targetKey: 'shortcut.app.show_mini_window' - }, - { - originalKey: 'shortcuts.selection_assistant_toggle', - targetKey: 'shortcut.selection.toggle_enabled' - }, - { - originalKey: 'shortcuts.selection_assistant_select_text', - targetKey: 'shortcut.selection.get_text' - }, - { - originalKey: 'shortcuts.new_topic', - targetKey: 'shortcut.topic.new' - }, - { - originalKey: 'shortcuts.toggle_show_assistants', - targetKey: 'shortcut.app.toggle_show_assistants' - }, - { - originalKey: 'shortcuts.copy_last_message', - targetKey: 'shortcut.chat.copy_last_message' - }, - { - originalKey: 'shortcuts.search_message_in_chat', - targetKey: 'shortcut.chat.search_message' - }, - { - originalKey: 'shortcuts.search_message', - targetKey: 'shortcut.app.search_message' - }, - { - originalKey: 'shortcuts.clear_topic', - targetKey: 'shortcut.chat.clear' - }, - { - originalKey: 'shortcuts.toggle_new_context', - targetKey: 'shortcut.chat.toggle_new_context' - }, - { - originalKey: 'shortcuts.exit_fullscreen', - targetKey: 'shortcut.app.exit_fullscreen' - } - ] -} as const - -// === AUTO-GENERATED CONTENT END === - -/** - * 映射统计: - * - ElectronStore项: 1 - * - Redux Store项: 175 - * - Redux分类: settings, selectionStore, nutstore, shortcuts - * - 总配置项: 176 - * - * 使用说明: - * 1. ElectronStore读取: configManager.get(mapping.originalKey) - * 2. Redux读取: 需要解析嵌套路径 reduxData[category][originalKey路径] - * 3. 默认值: 从defaultPreferences.default[mapping.targetKey]获取 - */ diff --git a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts b/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts deleted file mode 100644 index 495570511b..0000000000 --- a/src/main/data/migrate/dataRefactor/migrators/PreferencesMigrator.ts +++ /dev/null @@ -1,642 +0,0 @@ -import { dbService } from '@data/db/DbService' -import { preferenceTable } from '@data/db/schemas/preference' -import { loggerService } from '@logger' -import { DefaultPreferences } from '@shared/data/preference/preferenceSchemas' -import { and, eq } from 'drizzle-orm' - -import { configManager } from '../../../../services/ConfigManager' -import type { DataRefactorMigrateService } from '../DataRefactorMigrateService' -import { ELECTRON_STORE_MAPPINGS, REDUX_STORE_MAPPINGS } from './PreferencesMappings' - -const logger = loggerService.withContext('PreferencesMigrator') - -export interface MigrationItem { - originalKey: string - targetKey: string - type: string - defaultValue: any - source: 'electronStore' | 'redux' - sourceCategory?: string // Optional for electronStore -} - -export interface MigrationResult { - success: boolean - migratedCount: number - errors: Array<{ - key: string - error: string - }> -} - -export interface PreparedMigrationData { - targetKey: string - value: any - source: 'electronStore' | 'redux' - originalKey: string - sourceCategory?: string -} - -export interface BatchMigrationResult { - newPreferences: PreparedMigrationData[] - updatedPreferences: PreparedMigrationData[] - skippedCount: number - preparationErrors: Array<{ - key: string - error: string - }> -} - -export class PreferencesMigrator { - private db = dbService.getDb() - private migrateService: DataRefactorMigrateService - - constructor(migrateService: DataRefactorMigrateService) { - this.migrateService = migrateService - } - - /** - * Execute preferences migration from all sources using batch operations and transactions - */ - async migrate(onProgress?: (progress: number, message: string) => void): Promise { - logger.info('Starting preferences migration with batch operations') - - const result: MigrationResult = { - success: true, - migratedCount: 0, - errors: [] - } - - try { - // Phase 1: Prepare all migration data in memory (50% of progress) - onProgress?.(10, 'Loading migration items...') - const migrationItems = await this.loadMigrationItems() - logger.info(`Found ${migrationItems.length} items to migrate`) - - onProgress?.(25, 'Preparing migration data...') - const batchResult = await this.prepareMigrationData(migrationItems, (progress) => { - // Map preparation progress to 25-50% of total progress - const totalProgress = 25 + Math.floor(progress * 0.25) - onProgress?.(totalProgress, 'Preparing migration data...') - }) - - // Add preparation errors to result - result.errors.push(...batchResult.preparationErrors) - - if (batchResult.preparationErrors.length > 0) { - logger.warn('Some items failed during preparation', { - errorCount: batchResult.preparationErrors.length - }) - } - - // Phase 2: Execute batch migration in transaction (50% of progress) - onProgress?.(50, 'Executing batch migration...') - - const totalOperations = batchResult.newPreferences.length + batchResult.updatedPreferences.length - if (totalOperations > 0) { - try { - await this.executeBatchMigration(batchResult, (progress) => { - // Map execution progress to 50-90% of total progress - const totalProgress = 50 + Math.floor(progress * 0.4) - onProgress?.(totalProgress, 'Executing batch migration...') - }) - - result.migratedCount = totalOperations - logger.info('Batch migration completed successfully', { - newPreferences: batchResult.newPreferences.length, - updatedPreferences: batchResult.updatedPreferences.length, - skippedCount: batchResult.skippedCount - }) - } catch (batchError) { - logger.error('Batch migration transaction failed - all changes rolled back', batchError as Error) - result.success = false - result.errors.push({ - key: 'batch_migration', - error: `Transaction failed: ${batchError instanceof Error ? batchError.message : String(batchError)}` - }) - // Note: No need to manually rollback - transaction handles this automatically - } - } else { - logger.info('No preferences to migrate') - } - - onProgress?.(100, 'Migration completed') - - // Set success based on whether we had any critical errors - result.success = result.errors.length === 0 - - logger.info('Preferences migration completed', { - migratedCount: result.migratedCount, - errorCount: result.errors.length, - skippedCount: batchResult.skippedCount - }) - } catch (error) { - logger.error('Preferences migration failed', error as Error) - result.success = false - result.errors.push({ - key: 'global', - error: error instanceof Error ? error.message : String(error) - }) - } - - return result - } - - /** - * Load migration items from generated mapping relationships - * This uses the auto-generated PreferencesMappings.ts file - */ - private async loadMigrationItems(): Promise { - logger.info('Loading migration items from generated mappings') - const items: MigrationItem[] = [] - - // 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' - }) - }) - - // 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' - }) - }) - }) - - 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 - } - - /** - * Prepare all migration data in memory before database operations - * This phase reads all source data and performs conversions/validations - */ - private async prepareMigrationData( - migrationItems: MigrationItem[], - onProgress?: (progress: number) => void - ): Promise { - logger.info('Starting migration data preparation', { itemCount: migrationItems.length }) - - const batchResult: BatchMigrationResult = { - newPreferences: [], - updatedPreferences: [], - skippedCount: 0, - preparationErrors: [] - } - - // Get existing preferences to determine which are new vs updated - const existingPreferences = await this.getExistingPreferences() - const existingKeys = new Set(existingPreferences.map((p) => p.key)) - - // Process each migration item - for (let i = 0; i < migrationItems.length; i++) { - const item = migrationItems[i] - - try { - // Read original value from source - let originalValue: any - 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}`) - } - - // Determine value to migrate - let valueToMigrate = originalValue - let shouldSkip = false - - if (originalValue === undefined || originalValue === null) { - if (item.defaultValue !== null && item.defaultValue !== undefined) { - valueToMigrate = item.defaultValue - logger.debug('Using default value for preparation', { - targetKey: item.targetKey, - source: item.source, - originalKey: item.originalKey - }) - } else { - shouldSkip = true - batchResult.skippedCount++ - logger.debug('Skipping item - no data and no meaningful default', { - targetKey: item.targetKey, - source: item.source, - originalKey: item.originalKey - }) - } - } - - if (!shouldSkip) { - // Convert value to appropriate type - const convertedValue = this.convertValue(valueToMigrate, item.type) - - // Create prepared migration data - const preparedData: PreparedMigrationData = { - targetKey: item.targetKey, - value: convertedValue, - source: item.source, - originalKey: item.originalKey, - sourceCategory: item.sourceCategory - } - - // Categorize as new or updated - if (existingKeys.has(item.targetKey)) { - batchResult.updatedPreferences.push(preparedData) - } else { - batchResult.newPreferences.push(preparedData) - } - - logger.debug('Prepared migration data', { - targetKey: item.targetKey, - isUpdate: existingKeys.has(item.targetKey), - source: item.source - }) - } - } catch (error) { - logger.error('Failed to prepare migration item', { item, error }) - batchResult.preparationErrors.push({ - key: item.originalKey, - error: error instanceof Error ? error.message : String(error) - }) - } - - // Report progress - const progress = Math.floor(((i + 1) / migrationItems.length) * 100) - onProgress?.(progress) - } - - logger.info('Migration data preparation completed', { - newPreferences: batchResult.newPreferences.length, - updatedPreferences: batchResult.updatedPreferences.length, - skippedCount: batchResult.skippedCount, - errorCount: batchResult.preparationErrors.length - }) - - return batchResult - } - - /** - * Get all existing preferences from database to determine new vs updated items - */ - private async getExistingPreferences(): Promise> { - try { - const preferences = await this.db - .select({ - key: preferenceTable.key, - value: preferenceTable.value - }) - .from(preferenceTable) - .where(eq(preferenceTable.scope, 'default')) - - logger.debug('Loaded existing preferences', { count: preferences.length }) - return preferences - } catch (error) { - logger.error('Failed to load existing preferences', error as Error) - return [] - } - } - - /** - * Execute batch migration using database transaction with bulk operations - */ - private async executeBatchMigration( - batchData: BatchMigrationResult, - onProgress?: (progress: number) => void - ): Promise { - logger.info('Starting batch migration execution', { - newCount: batchData.newPreferences.length, - updateCount: batchData.updatedPreferences.length - }) - - // Validate batch data before starting transaction - this.validateBatchData(batchData) - - await this.db.transaction(async (tx) => { - const scope = 'default' - const timestamp = Date.now() - let completedOperations = 0 - const totalOperations = batchData.newPreferences.length + batchData.updatedPreferences.length - - // Batch insert new preferences - if (batchData.newPreferences.length > 0) { - logger.debug('Executing batch insert for new preferences', { count: batchData.newPreferences.length }) - - const insertValues = batchData.newPreferences.map((item) => ({ - scope, - key: item.targetKey, - value: item.value, - createdAt: timestamp, - updatedAt: timestamp - })) - - await tx.insert(preferenceTable).values(insertValues) - - completedOperations += batchData.newPreferences.length - const progress = Math.floor((completedOperations / totalOperations) * 100) - onProgress?.(progress) - - logger.info('Batch insert completed', { insertedCount: batchData.newPreferences.length }) - } - - // Batch update existing preferences - if (batchData.updatedPreferences.length > 0) { - logger.debug('Executing batch updates for existing preferences', { count: batchData.updatedPreferences.length }) - - // Execute updates in batches to avoid SQL limitations - const BATCH_SIZE = 50 - const updateBatches = this.chunkArray(batchData.updatedPreferences, BATCH_SIZE) - - for (const batch of updateBatches) { - // Use Promise.all to execute updates in parallel within the transaction - await Promise.all( - batch.map((item) => - tx - .update(preferenceTable) - .set({ - value: item.value, - updatedAt: timestamp - }) - .where(and(eq(preferenceTable.scope, scope), eq(preferenceTable.key, item.targetKey))) - ) - ) - - completedOperations += batch.length - const progress = Math.floor((completedOperations / totalOperations) * 100) - onProgress?.(progress) - } - - logger.info('Batch updates completed', { updatedCount: batchData.updatedPreferences.length }) - } - - logger.info('Transaction completed successfully', { - totalOperations: completedOperations, - newPreferences: batchData.newPreferences.length, - updatedPreferences: batchData.updatedPreferences.length - }) - }) - } - - /** - * Validate batch data before executing migration - */ - private validateBatchData(batchData: BatchMigrationResult): void { - const allData = [...batchData.newPreferences, ...batchData.updatedPreferences] - - // Check for duplicate target keys - const targetKeys = allData.map((item) => item.targetKey) - const duplicateKeys = targetKeys.filter((key, index) => targetKeys.indexOf(key) !== index) - - if (duplicateKeys.length > 0) { - throw new Error(`Duplicate target keys found in migration data: ${duplicateKeys.join(', ')}`) - } - - // Validate each item has required fields - for (const item of allData) { - if (!item.targetKey || item.targetKey.trim() === '') { - throw new Error(`Invalid targetKey found: '${item.targetKey}'`) - } - - if (item.value === undefined) { - throw new Error(`Undefined value for targetKey: '${item.targetKey}'`) - } - } - - logger.debug('Batch data validation passed', { - totalItems: allData.length, - uniqueKeys: targetKeys.length - }) - } - - /** - * Split array into chunks of specified size for batch processing - */ - private chunkArray(array: T[], chunkSize: number): T[][] { - const chunks: T[][] = [] - for (let i = 0; i < array.length; i += chunkSize) { - chunks.push(array.slice(i, i + chunkSize)) - } - return chunks - } - - /** - * Read value from ElectronStore (via ConfigManager) - */ - private async readFromElectronStore(key: string): Promise { - try { - return configManager.get(key) - } catch (error) { - logger.warn('Failed to read from ElectronStore', { key, error }) - return undefined - } - } - - /** - * Read value from Redux persist data with support for nested paths - */ - private async readFromReduxPersist(category: string, key: string): Promise { - try { - // Get cached Redux data from migrate service - const reduxData = this.migrateService?.getReduxData() - - 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 cache', { category, key, error }) - return undefined - } - } - - /** - * Convert value to the specified type - */ - private convertValue(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: - return value - } - } catch (error) { - logger.warn('Type conversion failed, using original value', { 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) - return isNaN(parsed) ? 0 : 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 } - } -} diff --git a/src/main/index.ts b/src/main/index.ts index ef3d908871..7ae5564b67 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -195,17 +195,6 @@ if (!app.requestSingleInstanceLock()) { // Initialize CacheService await cacheService.initialize() - // // Create two test windows for cross-window preference sync testing - // logger.info('Creating test windows for PreferenceService cross-window sync testing') - // const testWindow1 = dataRefactorMigrateService.createTestWindow() - // const testWindow2 = dataRefactorMigrateService.createTestWindow() - - // // Position windows to avoid overlap - // testWindow1.once('ready-to-show', () => { - // const [x, y] = testWindow1.getPosition() - // testWindow2.setPosition(x + 50, y + 50) - // }) - /************FOR TESTING ONLY END****************/ // Record current version for tracking diff --git a/src/renderer/dataRefactorMigrate.html b/src/renderer/dataRefactorMigrate.html deleted file mode 100644 index 6b0287db2c..0000000000 --- a/src/renderer/dataRefactorMigrate.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - Cherry Studio - Data Refactor Migration - - - - - - - - - diff --git a/src/renderer/dataRefactorTest.html b/src/renderer/dataRefactorTest.html deleted file mode 100644 index 9a8c31a041..0000000000 --- a/src/renderer/dataRefactorTest.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - Data Refactor Test Window - PreferenceService Testing - - - - - - - - - diff --git a/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx b/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx deleted file mode 100644 index eb3c538434..0000000000 --- a/src/renderer/src/windows/dataRefactorMigrate/MigrateApp.tsx +++ /dev/null @@ -1,562 +0,0 @@ -import { Button } from '@cherrystudio/ui' -import { getToastUtilities } from '@renderer/components/TopView/toast' -import { AppLogo } from '@renderer/config/env' -import { loggerService } from '@renderer/services/LoggerService' -import { IpcChannel } from '@shared/IpcChannel' -import { 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 - | 'backup_progress' // Backup in progress - user is backing up - | 'backup_confirmed' // Backup confirmed - ready to migrate - | 'migration' // Migration in progress - cannot cancel - | 'completed' // Completed - restart app - | 'error' // Error - recovery options - -interface MigrationProgress { - stage: MigrationStage - progress: number - total: number - message: string - error?: string -} - -const MigrateApp: React.FC = () => { - const [progress, setProgress] = useState({ - stage: 'introduction', - progress: 0, - total: 100, - message: 'Ready to start data migration' - }) - - useEffect(() => { - window.toast = getToastUtilities() - }, []) - - useEffect(() => { - // Listen for progress updates - const handleProgress = (_: any, progressData: MigrationProgress) => { - setProgress(progressData) - } - - window.electron.ipcRenderer.on(IpcChannel.DataMigrateProgress, handleProgress) - - // Request initial progress - window.electron.ipcRenderer - .invoke(IpcChannel.DataMigrate_GetProgress) - .then((initialProgress: MigrationProgress) => { - if (initialProgress) { - setProgress(initialProgress) - } - }) - - return () => { - window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrateProgress) - } - }, []) - - const currentStep = useMemo(() => { - switch (progress.stage) { - case 'introduction': - return 0 - case 'backup_required': - case 'backup_progress': - case 'backup_confirmed': - return 1 - case 'migration': - return 2 - case 'completed': - return 3 - case 'error': - return -1 // Error state - will be handled separately - default: - return 0 - } - }, [progress.stage]) - - const stepStatus = useMemo(() => { - if (progress.stage === 'error') { - return 'error' - } - 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 = 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 = () => { - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_RestartApp) - } - - const handleCloseWindow = () => { - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_CloseWindow) - } - - const handleCancel = () => { - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_Cancel) - } - - const handleShowBackupDialog = () => { - // Open the main window backup dialog - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_ShowBackupDialog) - } - - const handleBackupCompleted = () => { - // Notify the main process that backup is completed - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_BackupCompleted) - } - - const handleRetryMigration = () => { - window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_RetryMigration) - } - - const getProgressColor = () => { - switch (progress.stage) { - case 'completed': - return 'var(--color-primary)' - case 'error': - return '#ff4d4f' - case 'backup_confirmed': - return 'var(--color-primary)' - default: - return 'var(--color-primary)' - } - } - - const getCurrentStepIcon = () => { - switch (progress.stage) { - case 'introduction': - return - case 'backup_required': - case 'backup_progress': - return - case 'backup_confirmed': - return - case 'migration': - return ( - - - - ) - case 'completed': - return - case 'error': - return - default: - return - } - } - - const renderActionButtons = () => { - switch (progress.stage) { - case 'introduction': - return ( - <> - - - - - ) - case 'backup_required': - return ( - <> - - - - - - ) - case 'backup_confirmed': - return ( - - - - - - - ) - case 'migration': - return ( - -
- -
- ) - case 'completed': - return ( - -
- -
- ) - case 'error': - return ( - - - - - - - ) - default: - return null - } - } - - return ( - - {/* Header */} -
- - - 数据迁移向导 -
- - {/* Main Content */} - - {/* Left Sidebar with Steps */} - - - - - - - {/* Right Content Area */} - - - {getCurrentStepIcon()} - - {progress.stage === 'introduction' && ( - - 将数据迁移到新的架构中 - - Cherry Studio对数据的存储和使用方式进行了重大重构,在新的架构下,效率和安全性将会得到极大提升。 -
-
- 数据必须进行迁移,才能在新版本中使用。 -
-
- 我们会指导你完成迁移,迁移过程不会损坏原来的数据,你随时可以取消迁移,并继续使用旧版本。 -
- {/* Debug button to test Redux data extraction */} -
- -
-
- )} - - {progress.stage === 'backup_required' && ( - - 创建数据备份 - - 迁移前必须创建数据备份以确保数据安全。请选择备份位置或确认已有最新备份。 - - - )} - - {progress.stage === 'backup_progress' && ( - - 准备数据备份 - 请选择备份位置,保存后等待备份完成。 - - )} - - {progress.stage === 'backup_confirmed' && ( - - 备份完成 - - 数据备份已完成,现在可以安全地开始迁移。 - - - )} - - {progress.stage === 'error' && ( - - 迁移失败 - - 迁移过程遇到错误,您可以重新尝试或继续使用之前版本(原始数据完好保存)。 -
-
- 错误信息:{progress.error} -
-
- )} - - {progress.stage === 'completed' && ( - - 迁移完成 - 数据已成功迁移,重启应用后即可正常使用。 - - )} - - {(progress.stage == 'backup_progress' || progress.stage == 'migration') && ( - - - - )} -
-
-
- - {/* Footer */} -
{renderActionButtons()}
-
- ) -} - -const Container = styled.div` - width: 100%; - height: 100vh; - display: flex; - flex-direction: column; - background: #fff; -` - -const Header = styled.div` - height: 48px; - background: rgb(240, 240, 240); - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - -webkit-app-region: drag; - user-select: none; -` - -const HeaderTitle = styled.div` - font-size: 16px; - font-weight: 600; - color: black; - margin-left: 12px; -` - -const HeaderLogo = styled.img` - width: 24px; - height: 24px; - border-radius: 6px; -` - -const MainContent = styled.div` - flex: 1; - display: flex; - overflow: hidden; -` - -const LeftSidebar = styled.div` - width: 150px; - background: #fff; - border-right: 1px solid #f0f0f0; - display: flex; - flex-direction: column; -` - -const StepsContainer = styled.div` - padding: 32px 24px; - flex: 1; - - .ant-steps-item-process .ant-steps-item-icon { - background-color: var(--color-primary); - border-color: var(--color-primary-soft); - } - - .ant-steps-item-finish .ant-steps-item-icon { - background-color: var(--color-primary-mute); - border-color: var(--color-primary-mute); - } - - .ant-steps-item-finish .ant-steps-item-icon > .ant-steps-icon { - color: var(--color-primary); - } - - .ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon { - color: #fff; - } - - .ant-steps-item-wait .ant-steps-item-icon { - border-color: #d9d9d9; - } -` - -const RightContent = styled.div` - flex: 1; - display: flex; - flex-direction: column; -` - -const ContentArea = styled.div` - flex: 1; - display: flex; - flex-direction: column; - /* justify-content: center; */ - /* align-items: center; */ - /* margin: 0 auto; */ - width: 100%; - padding: 24px; -` - -const Footer = styled.div` - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - - background: rgb(250, 250, 250); - - height: 64px; - - padding: 0 24px; - gap: 16px; -` - -const Spacer = styled.div` - flex: 1; -` - -const ProgressContainer = styled.div` - margin: 32px 0; - width: 100%; -` - -const ButtonRow = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - min-width: 300px; -` - -const InfoIcon = styled.div` - display: flex; - justify-content: center; - align-items: center; - margin-top: 12px; -` - -const InfoCard = styled.div<{ variant?: 'info' | 'warning' | 'success' | 'error' }>` - width: 100%; -` - -const InfoTitle = styled.div` - margin-bottom: 32px; - margin-top: 32px; - font-size: 16px; - font-weight: 600; - color: var(--color-primary); - line-height: 1.4; - text-align: center; -` - -const InfoDescription = styled.p` - margin: 0; - color: rgba(0, 0, 0, 0.68); - line-height: 1.8; - max-width: 420px; - margin: 0 auto; -` - -const SpinningIcon = styled.div` - display: inline-block; - animation: spin 2s linear infinite; - - @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } -` - -export default MigrateApp diff --git a/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx b/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx deleted file mode 100644 index 7c8902a402..0000000000 --- a/src/renderer/src/windows/dataRefactorMigrate/entryPoint.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import '@ant-design/v5-patch-for-react-19' -import '@renderer/assets/styles/index.css' - -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() diff --git a/src/renderer/src/windows/dataRefactorTest/README.md b/src/renderer/src/windows/dataRefactorTest/README.md deleted file mode 100644 index 104fbac7a3..0000000000 --- a/src/renderer/src/windows/dataRefactorTest/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# 数据重构项目测试窗口 - -专用于测试数据重构项目各项功能的独立测试窗口系统,包括 PreferenceService、CacheService、DataApiService 和相关 React hooks。 - -## 🎯 当前实现 - -✅ **已完成的功能**: - -- 专用的测试窗口 (DataRefactorTestWindow) -- **双窗口启动**:应用启动时会同时打开主窗口和两个测试窗口 -- **跨窗口同步测试**:两个测试窗口可以相互验证偏好设置的实时同步 -- **实时UI联动**:主题、语言、缩放等偏好设置变化会立即反映在UI上 -- **🎛️ Slider联动测试**:多个交互式滑块控制数值类型偏好设置,支持跨窗口实时同步 -- **多源窗口编号**:支持URL参数、窗口标题、窗口名称等多种方式确定窗口编号 -- 完整的测试界面,包含4个专业测试组件 -- 自动窗口定位,避免重叠 -- 窗口编号标识,便于区分 - -## 测试组件 - -## PreferenceService 测试模块 - -### 1. PreferenceService 基础测试 - -- 直接测试服务层API:get, set, getCachedValue, isCached, preload, getMultiple -- 支持各种数据类型:字符串、数字、布尔值、JSON对象 -- 实时显示操作结果 - -### 2. usePreference Hook 测试 - -- 测试单个偏好设置的React hooks -- 支持的测试键: - - `app.theme.mode` - 主题模式 - - `app.language` - 语言设置 - - `app.spell_check.enabled` - 拼写检查 - - `app.zoom_factor` - 缩放因子 (🎛️ 支持Slider) - - `chat.message.font_size` - 消息字体大小 (🎛️ 支持Slider) - - `feature.selection.action_window_opacity` - 选择窗口透明度 (🎛️ 支持Slider) -- 实时值更新和类型转换 -- **Slider联动控制**:数值类型偏好设置提供交互式滑块,支持实时拖拽调整 - -### 3. usePreferences 批量操作测试 - -- 测试多个偏好设置的批量管理 -- 5种预设场景:基础设置、UI设置、用户设置、🎛️数值设置、自定义组合 -- **🎛️ 数值设置场景**:专门的Slider联动控制区域,包含缩放、字体、选择窗口透明度三个滑块 -- 批量更新功能,支持JSON格式输入 -- 快速切换操作 - -### 4. Hook 高级功能测试 - -- 预加载机制测试 -- 订阅机制验证 -- 缓存管理测试 -- 性能测试 -- 多个hook实例同步测试 - -## CacheService 测试模块 - -### 1. CacheService 直接API测试 - -- **三层缓存架构测试**:Memory cache、Shared cache、Persist cache -- **基础操作**: get, set, has, delete 方法的完整测试 -- **TTL支持**: 可配置的过期时间测试(2s、5s、10s) -- **跨窗口同步**: Shared cache 和 Persist cache 的实时同步验证 -- **数据类型支持**: 字符串、数字、对象、数组等多种数据类型 -- **性能优化**: 显示操作计数和自动刷新机制 - -### 2. Cache Hooks 基础测试 - -- **useCache Hook**: 测试内存缓存的React集成 - - 默认值自动设置 - - 实时值更新和类型安全 - - Hook生命周期管理 -- **useSharedCache Hook**: 测试跨窗口缓存同步 - - 跨窗口实时同步验证 - - 广播机制测试 - - 并发更新处理 -- **usePersistCache Hook**: 测试持久化缓存 - - 类型安全的预定义Schema - - localStorage持久化 - - 默认值回退机制 -- **数据类型测试**: - - 数字类型滑块控制 - - 复杂对象结构更新 - - 实时渲染统计 - -### 3. Cache 高级功能测试 - -- **TTL过期机制**: - - 实时倒计时进度条 - - 自动过期验证 - - 懒加载清理机制 -- **Hook引用保护**: - - 活跃Hook的key删除保护 - - 引用计数验证 - - 错误处理测试 -- **深度相等性优化**: - - 相同引用跳过测试 - - 相同内容深度比较 - - 性能优化验证 -- **性能测试**: - - 快速更新测试(100次/秒) - - 订阅触发统计 - - 渲染次数监控 -- **多Hook同步**: - - 同一key的多个hook实例 - - 跨缓存类型同步测试 - -### 4. Cache 压力测试 - -- **快速操作测试**: - - 1000次操作/10秒高频测试 - - 每秒操作数统计 - - 错误率监控 -- **并发更新测试**: - - 多个Hook同时更新 - - 跨窗口并发处理 - - 数据一致性验证 -- **大数据测试**: - - 10KB、100KB、1MB对象存储 - - 内存使用估算 - - 存储限制警告 -- **存储限制测试**: - - localStorage容量测试 - - 缓存大小监控 - - 性能影响评估 - -## 启动方式 - -**自动启动**:应用正常启动时会自动创建两个测试窗口,窗口会自动错位显示避免重叠 - -**手动启动**: - -```javascript -// 在开发者控制台中执行 - 创建单个测试窗口 -const { dataRefactorMigrateService } = require('./out/main/data/migrate/dataRefactor/DataRefactorMigrateService') -dataRefactorMigrateService.createTestWindow() - -// 创建多个测试窗口 -dataRefactorMigrateService.createTestWindow() // 第二个窗口 -dataRefactorMigrateService.createTestWindow() // 第三个窗口... - -// 关闭所有测试窗口 -dataRefactorMigrateService.closeTestWindows() -``` - -## 文件结构 - -``` -src/renderer/src/windows/dataRefactorTest/ -├── entryPoint.tsx # 窗口入口 -├── TestApp.tsx # 主应用组件 -└── components/ - # PreferenceService 测试组件 - ├── PreferenceServiceTests.tsx # 服务层测试 - ├── PreferenceBasicTests.tsx # 基础Hook测试 - ├── PreferenceHookTests.tsx # 高级Hook测试 - ├── PreferenceMultipleTests.tsx # 批量操作测试 - - # CacheService 测试组件 - ├── CacheServiceTests.tsx # 直接API测试 - ├── CacheBasicTests.tsx # Hook基础测试 - ├── CacheAdvancedTests.tsx # 高级功能测试 - ├── CacheStressTests.tsx # 压力测试 - - # DataApiService 测试组件 - ├── DataApiBasicTests.tsx # 基础CRUD测试 - ├── DataApiAdvancedTests.tsx # 高级功能测试 - ├── DataApiHookTests.tsx # React Hooks测试 - └── DataApiStressTests.tsx # 压力测试 -``` - -## 跨窗口同步测试 - -🔄 **测试场景**: - -### PreferenceService 跨窗口同步 -1. **实时同步验证**:在窗口#1中修改某个偏好设置,立即观察窗口#2是否同步更新 -2. **并发修改测试**:在两个窗口中快速连续修改同一设置,验证数据一致性 -3. **批量操作同步**:在一个窗口中批量更新多个设置,观察另一个窗口的同步表现 -4. **Hook实例同步**:验证多个usePreference hook实例是否正确同步 - -### CacheService 跨窗口同步 -1. **Shared Cache同步**:在窗口#1中设置共享缓存,观察窗口#2的实时更新 -2. **Persist Cache同步**:修改持久化缓存,验证所有窗口的localStorage同步 -3. **TTL跨窗口验证**:在一个窗口设置TTL,观察其他窗口的过期行为 -4. **并发缓存操作**:多窗口同时操作同一缓存key,验证数据一致性 -5. **Hook引用保护**:在一个窗口尝试删除其他窗口正在使用的缓存key - -📋 **测试步骤**: - -### PreferenceService 测试步骤 -1. 同时打开两个测试窗口(自动启动) -2. 选择相同的偏好设置键进行测试 -3. 在窗口#1中修改值,观察窗口#2的反应 -4. 检查"Hook 高级功能测试"中的订阅触发次数是否增加 -5. 验证缓存状态和实时数据的一致性 - -### CacheService 测试步骤 -1. 同时打开两个测试窗口(自动启动) -2. **Memory Cache测试**:仅在当前窗口有效,其他窗口不受影响 -3. **Shared Cache测试**:在窗口#1设置共享缓存,立即检查窗口#2是否同步 -4. **Persist Cache测试**:修改持久化缓存,验证localStorage和跨窗口同步 -5. **TTL测试**:设置带过期时间的缓存,观察倒计时和跨窗口过期行为 -6. **压力测试**:运行高频操作,监控性能指标和错误率 -7. **引用保护测试**:在Hook活跃时尝试删除key,验证保护机制 - -## 注意事项 - -⚠️ **重要警告**: - -### PreferenceService 警告 -- **真实数据库存储**:测试使用真实的偏好设置系统 -- **跨应用同步**:修改的值会同步到主应用和所有测试窗口 -- **持久化影响**:所有更改都会持久化到SQLite数据库 - -### CacheService 警告 -- **内存占用**:压力测试可能消耗大量内存,影响浏览器性能 -- **localStorage影响**:大数据测试会占用浏览器存储空间(最大5-10MB) -- **性能影响**:高频操作测试可能短暂影响UI响应性 -- **跨窗口影响**:Shared和Persist缓存会影响所有打开的窗口 -- **TTL清理**:过期缓存会自动清理,可能影响其他功能的测试数据 - -## 开发模式特性 - -- 自动打开DevTools便于调试 -- 支持热重载 -- 完整的TypeScript类型检查 -- React DevTools支持 - -## 💡 快速开始 - -### PreferenceService 快速测试 -1. **启动应用** - 自动打开2个测试窗口 -2. **选择测试** - 在"usePreference Hook 测试"中选择要测试的偏好设置键 -3. **🎛️ Slider联动测试** - 选择数值类型偏好设置,拖动Slider观察实时变化 -4. **跨窗口验证** - 在窗口#1中修改值,观察窗口#2是否同步 -5. **批量Slider测试** - 切换到"数值设置场景",同时拖动多个滑块测试批量同步 -6. **高级测试** - 使用"Hook 高级功能测试"验证订阅和缓存机制 - -### CacheService 快速测试 -1. **基础操作** - 使用"CacheService 直接API测试"进行get/set/delete操作 -2. **Hook测试** - 在"Cache Hooks 基础测试"中测试不同数据类型和默认值 -3. **TTL验证** - 设置2秒TTL缓存,观察实时倒计时和自动过期 -4. **跨窗口同步** - 设置Shared Cache,在另一窗口验证实时同步 -5. **持久化测试** - 修改Persist Cache,刷新页面验证localStorage持久化 -6. **压力测试** - 运行"快速操作测试",观察高频操作的性能表现 -7. **引用保护** - 启用Hook后尝试删除key,验证保护机制 - -## 🔧 技术实现 - -### 基础架构 -- **窗口管理**: DataRefactorMigrateService 单例管理多个测试窗口 -- **跨窗口识别**: 多源窗口编号支持,确保每个窗口都有唯一标识 -- **UI框架**: Ant Design + styled-components + React 18 -- **类型安全**: 完整的 TypeScript 类型检查和类型约束 - -### PreferenceService 技术实现 -- **数据同步**: 基于真实的 PreferenceService 和 IPC 通信 -- **实时主题**: 使用 useSyncExternalStore 实现主题、缩放等设置的实时UI响应 -- **类型约束**: 偏好设置键的完整TypeScript类型检查 - -### CacheService 技术实现 -- **三层缓存**: Memory (Map) + Shared (Map + IPC) + Persist (Map + localStorage) -- **React集成**: useSyncExternalStore 实现外部状态订阅 -- **性能优化**: Object.is() 浅比较 + 深度相等性检查,跳过无效更新 -- **TTL管理**: 懒加载过期检查,基于时间戳的精确控制 -- **IPC同步**: 跨进程消息广播,支持批量操作和增量更新 -- **引用跟踪**: Set-based Hook引用计数,防止意外删除 -- **错误处理**: 完善的try-catch机制和用户友好的错误提示 -- **内存管理**: 自动清理、定时器管理和资源释放 diff --git a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx b/src/renderer/src/windows/dataRefactorTest/TestApp.tsx deleted file mode 100644 index 61bea71178..0000000000 --- a/src/renderer/src/windows/dataRefactorTest/TestApp.tsx +++ /dev/null @@ -1,448 +0,0 @@ -import { getToastUtilities } from '@renderer/components/TopView/toast' -import { AppLogo } from '@renderer/config/env' -import { usePreference } from '@renderer/data/hooks/usePreference' -import { loggerService } from '@renderer/services/LoggerService' -import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Card, Col, Divider, Layout, Row, Space, Tabs, Typography } from 'antd' -import { Activity, AlertTriangle, Database, FlaskConical, Settings, TestTube, TrendingUp, Zap } from 'lucide-react' -import React, { useEffect } from 'react' -import styled from 'styled-components' - -import CacheAdvancedTests from './components/CacheAdvancedTests' -import CacheBasicTests from './components/CacheBasicTests' -import CacheServiceTests from './components/CacheServiceTests' -import CacheStressTests from './components/CacheStressTests' -import DataApiAdvancedTests from './components/DataApiAdvancedTests' -import DataApiBasicTests from './components/DataApiBasicTests' -import DataApiHookTests from './components/DataApiHookTests' -import DataApiStressTests from './components/DataApiStressTests' -import PreferenceBasicTests from './components/PreferenceBasicTests' -import PreferenceHookTests from './components/PreferenceHookTests' -import PreferenceMultipleTests from './components/PreferenceMultipleTests' -import PreferenceServiceTests from './components/PreferenceServiceTests' - -const { Header, Content } = Layout -const { Title, Text } = Typography - -const logger = loggerService.withContext('TestApp') - -const TestApp: React.FC = () => { - // Get window number from multiple sources - const getWindowNumber = () => { - // Try URL search params first - const urlParams = new URLSearchParams(window.location.search) - const windowParam = urlParams.get('window') - if (windowParam) { - return windowParam - } - - // Try document title - const windowTitle = document.title - const windowMatch = windowTitle.match(/#(\d+)/) - if (windowMatch) { - return windowMatch[1] - } - - // Try window name - if (window.name && window.name.includes('#')) { - const nameMatch = window.name.match(/#(\d+)/) - if (nameMatch) { - return nameMatch[1] - } - } - - // Fallback: generate based on window creation time - return Math.floor(Date.now() / 1000) % 100 - } - - useEffect(() => { - window.toast = getToastUtilities() - }, []) - - const windowNumber = getWindowNumber() - - // Add theme preference monitoring for UI changes - const [theme, setTheme] = usePreference('ui.theme_mode') - const [language] = usePreference('app.language') - const [zoomFactor] = usePreference('app.zoom_factor') - - // Apply theme-based styling - const isDarkTheme = theme === ThemeMode.dark - const headerBg = isDarkTheme ? '#141414' : '#fff' - const borderColor = isDarkTheme ? '#303030' : '#f0f0f0' - const textColor = isDarkTheme ? '#fff' : '#000' - - // Apply zoom factor - const zoomValue = typeof zoomFactor === 'number' ? zoomFactor : 1.0 - - return ( - -
- - - Logo - - Test Window #{windowNumber} {isDarkTheme ? '🌙' : '☀️'} - - - - - - Cross-Window Sync Testing | {language || 'en-US'} | Zoom: {Math.round(zoomValue * 100)}% - - - -
- - - - - {/* Introduction Card */} - - - - - - - 数据重构项目测试套件 (窗口 #{windowNumber}) - - - - 此测试窗口用于验证数据重构项目的各项功能,包括 PreferenceService、CacheService、DataApiService - 和相关 React hooks 的完整测试套件。 - - - PreferenceService 测试使用真实的偏好设置系统,CacheService 测试使用三层缓存架构,DataApiService - 测试使用专用的测试路由和假数据。 - - - 📋 跨窗口测试指南:在一个窗口中修改偏好设置,观察其他窗口是否实时同步更新。 - - - 🗄️ 缓存系统测试:三层缓存架构(Memory/Shared/Persist),支持跨窗口同步、TTL过期、性能优化。 - - - 🚀 数据API测试:包含基础CRUD、高级功能、React hooks和压力测试,全面验证数据请求架构。 - - - - - - {/* Main Content Tabs */} - - - - PreferenceService 测试 - - ), - children: ( - - {/* PreferenceService Basic Tests */} - - - - PreferenceService 基础测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* Basic Hook Tests */} - - - - usePreference Hook 测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* Hook Tests */} - - - - Hook 高级功能测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* Multiple Preferences Tests */} - - - - usePreferences 批量操作测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - ) - }, - { - key: 'cache', - label: ( - - - CacheService 测试 - - ), - children: ( - - {/* Cache Service Tests */} - - - - CacheService 直接API测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* Cache Basic Tests */} - - - - Cache Hooks 基础测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* Cache Advanced Tests */} - - - - Cache 高级功能测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* Cache Stress Tests */} - - - - Cache 压力测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - ) - }, - { - key: 'dataapi', - label: ( - - - DataApiService 测试 - - ), - children: ( - - {/* DataApi Basic Tests */} - - - - DataApi 基础功能测试 (CRUD操作) - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* DataApi Advanced Tests */} - - - - DataApi 高级功能测试 (取消、重试、批量) - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* DataApi Hook Tests */} - - - - DataApi React Hooks 测试 - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - {/* DataApi Stress Tests */} - - - - DataApi 压力测试 (性能与错误处理) - - } - size="small" - style={{ backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', borderColor: borderColor }}> - - - - - ) - } - ]} - /> - - - - - - - - - - - - - -
- ) -} - -const HeaderContent = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - height: 100%; -` - -const Container = styled.div` - max-width: 1200px; - margin: 0 auto; -` - -const StyledTabs = styled(Tabs)<{ $isDark: boolean }>` - .ant-tabs-nav { - background: ${(props) => (props.$isDark ? '#262626' : '#fafafa')}; - border-radius: 6px 6px 0 0; - margin-bottom: 0; - } - - .ant-tabs-tab { - color: ${(props) => (props.$isDark ? '#d9d9d9' : '#666')} !important; - - &:hover { - color: ${(props) => (props.$isDark ? '#fff' : '#000')} !important; - } - - &.ant-tabs-tab-active { - color: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')} !important; - - .ant-tabs-tab-btn { - color: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')} !important; - } - } - } - - .ant-tabs-ink-bar { - background: ${(props) => (props.$isDark ? '#1890ff' : '#1890ff')}; - } - - .ant-tabs-content { - background: ${(props) => (props.$isDark ? '#1f1f1f' : '#fff')}; - border-radius: 0 0 6px 6px; - padding: 24px 0; - } - - .ant-tabs-tabpane { - color: ${(props) => (props.$isDark ? '#fff' : '#000')}; - } -` - -export default TestApp diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx deleted file mode 100644 index 657a152358..0000000000 --- a/src/renderer/src/windows/dataRefactorTest/components/CacheAdvancedTests.tsx +++ /dev/null @@ -1,482 +0,0 @@ -import { cacheService } from '@renderer/data/CacheService' -import { useCache, useSharedCache } from '@renderer/data/hooks/useCache' -import { usePreference } from '@renderer/data/hooks/usePreference' -import { loggerService } from '@renderer/services/LoggerService' -import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Badge, Button, Card, Col, Divider, message, Progress, Row, Space, Tag, Typography } from 'antd' -import { Activity, AlertTriangle, CheckCircle, Clock, Shield, Timer, XCircle, Zap } from 'lucide-react' -import React, { useCallback, useEffect, useRef, useState } from 'react' -import styled from 'styled-components' - -const { Text } = Typography - -const logger = loggerService.withContext('CacheAdvancedTests') - -/** - * Advanced cache testing component - * Tests TTL expiration, hook reference tracking, deep equality, performance - */ -const CacheAdvancedTests: React.FC = () => { - const [currentTheme] = usePreference('ui.theme_mode') - const isDarkTheme = currentTheme === ThemeMode.dark - - // TTL Testing - const [ttlKey] = useState('test-ttl-cache' as const) - const [ttlValue, setTtlValue] = useCache(ttlKey as any, 'test-ttl-cache') - const [ttlExpireTime, setTtlExpireTime] = useState(null) - const [ttlProgress, setTtlProgress] = useState(0) - - // Hook Reference Tracking - const [protectedKey] = useState('test-protected-cache' as const) - const [protectedValue] = useCache(protectedKey as any, 'protected-value') - const [deleteAttemptResult, setDeleteAttemptResult] = useState('') - - // Deep Equality Testing - const [deepEqualKey] = useState('test-deep-equal' as const) - const [objectValue, setObjectValue] = useCache(deepEqualKey as any, { nested: { count: 0 }, tags: ['initial'] }) - const [updateSkipCount] = useState(0) - - // Performance Testing - const [perfKey] = useState('test-performance' as const) - const [perfValue, setPerfValue] = useCache(perfKey as any, 0) - const [rapidUpdateCount, setRapidUpdateCount] = useState(0) - const [subscriptionTriggers, setSubscriptionTriggers] = useState(0) - const renderCountRef = useRef(0) - const [displayRenderCount, setDisplayRenderCount] = useState(0) - - // Multi-hook testing - const [multiKey] = useState('test-multi-hook' as const) - const [value1] = useCache(multiKey as any, 'hook-1-default') - const [value2] = useCache(multiKey as any, 'hook-2-default') - const [value3] = useSharedCache(multiKey as any, 'hook-3-shared') - - const intervalRef = useRef(null) - const performanceTestRef = useRef(null) - - // Update render count without causing re-renders - renderCountRef.current += 1 - - // Track subscription changes - useEffect(() => { - const unsubscribe = cacheService.subscribe(perfKey, () => { - setSubscriptionTriggers((prev) => prev + 1) - }) - return unsubscribe - }, [perfKey]) - - // TTL Testing Functions - const startTTLTest = useCallback( - (ttlMs: number) => { - const testValue = { message: 'TTL Test', timestamp: Date.now() } - cacheService.set(ttlKey, testValue, ttlMs) - setTtlValue(testValue.message) - - const expireAt = Date.now() + ttlMs - setTtlExpireTime(expireAt) - - // Clear previous interval - if (intervalRef.current) { - clearInterval(intervalRef.current) - } - - // Update progress every 100ms - intervalRef.current = setInterval(() => { - const now = Date.now() - const remaining = Math.max(0, expireAt - now) - const progress = Math.max(0, 100 - (remaining / ttlMs) * 100) - - setTtlProgress(progress) - - if (remaining <= 0) { - clearInterval(intervalRef.current!) - setTtlExpireTime(null) - message.info('TTL expired, checking value...') - - // Check if value is actually expired - setTimeout(() => { - const currentValue = cacheService.get(ttlKey) - if (currentValue === undefined) { - message.success('TTL expiration working correctly!') - } else { - message.warning('TTL expiration may have failed') - } - }, 100) - } - }, 100) - - message.info(`TTL test started: ${ttlMs}ms`) - logger.info('TTL test started', { key: ttlKey, ttl: ttlMs, expireAt }) - }, - [ttlKey, setTtlValue] - ) - - // Hook Reference Tracking Test - const testDeleteProtection = () => { - try { - const deleted = cacheService.delete(protectedKey) - setDeleteAttemptResult(deleted ? 'Deleted (unexpected!)' : 'Protected (expected)') - logger.info('Delete protection test', { key: protectedKey, deleted }) - } catch (error) { - setDeleteAttemptResult(`Error: ${(error as Error).message}`) - logger.error('Delete protection test error', error as Error) - } - } - - // Deep Equality Testing - const testDeepEquality = (operation: string) => { - const currentCount = updateSkipCount - - switch (operation) { - case 'same-reference': - // Set same reference - should skip - setObjectValue(objectValue as { nested: { count: number }; tags: string[] }) - break - - case 'same-content': - // Set same content but different reference - should skip with deep comparison - setObjectValue({ nested: { count: objectValue?.nested?.count || 0 }, tags: [...(objectValue?.tags || [])] }) - break - - case 'different-content': - // Set different content - should update - setObjectValue({ - nested: { count: (objectValue?.nested?.count || 0) + 1 }, - tags: [...(objectValue?.tags || []), `update-${Date.now()}`] - }) - break - } - - // Check if update count changed - setTimeout(() => { - if (currentCount === updateSkipCount) { - message.success('Update skipped due to equality check') - } else { - message.info('Update applied due to content change') - } - }, 100) - - logger.info('Deep equality test', { operation, currentCount, objectValue }) - } - - // Performance Testing - const startRapidUpdates = () => { - let count = 0 - const startTime = Date.now() - - performanceTestRef.current = setInterval(() => { - count++ - setPerfValue(count) - setRapidUpdateCount(count) - - if (count >= 100) { - clearInterval(performanceTestRef.current!) - const duration = Date.now() - startTime - message.success(`Rapid updates test completed: ${count} updates in ${duration}ms`) - logger.info('Rapid updates test completed', { count, duration }) - } - }, 10) // Update every 10ms - } - - const stopRapidUpdates = () => { - if (performanceTestRef.current) { - clearInterval(performanceTestRef.current) - message.info('Rapid updates test stopped') - } - } - - // Cleanup - useEffect(() => { - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current) - } - if (performanceTestRef.current) { - clearInterval(performanceTestRef.current) - } - } - }, []) - - return ( - - -
- - - Advanced Features • Renders: {displayRenderCount || renderCountRef.current} • Subscriptions:{' '} - {subscriptionTriggers} - - - -
- - - {/* TTL Testing */} - - - - TTL Expiration Testing -
- } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Key: {ttlKey} - - - - - - - - - {ttlExpireTime && ( -
- Expiration Progress: - = 100 ? 'success' : 'active'} - strokeColor={isDarkTheme ? '#1890ff' : undefined} - /> -
- )} - - - Current Value: -
{ttlValue ? JSON.stringify(ttlValue, null, 2) : 'undefined'}
-
-
- - - - {/* Hook Reference Tracking */} - - - - Hook Reference Protection - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Key: {protectedKey} - - - - - - {deleteAttemptResult && ( - {deleteAttemptResult} - )} - - - Current Value: -
{JSON.stringify(protectedValue, null, 2)}
-
-
-
- - - - - {/* Deep Equality Testing */} - - - - Deep Equality Optimization - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Key: {deepEqualKey} - - - Skip Count: - - - - - - - - - - Current Object: -
{JSON.stringify(objectValue, null, 2)}
-
-
-
- - - {/* Performance Testing */} - - - - Performance Testing - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Key: {perfKey} - - - Updates: - - - - - - - - - Performance Value: -
{JSON.stringify(perfValue, null, 2)}
-
-
-
- -
- - - - {/* Multi-Hook Synchronization */} - - - Multi-Hook Synchronization Test - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Testing multiple hooks using the same key: {multiKey} - - - - - - -
{JSON.stringify(value1, null, 2)}
-
-
- - - - -
{JSON.stringify(value2, null, 2)}
-
-
- - - - -
{JSON.stringify(value3, null, 2)}
-
-
- -
- - - - - -
-
- -
- - 💡 高级功能测试: TTL过期机制、Hook引用保护、深度相等性优化、性能测试、多Hook同步验证 - -
- -
- ) -} - -const TestContainer = styled.div<{ $isDark: boolean }>` - color: ${(props) => (props.$isDark ? '#fff' : '#000')}; -` - -const ResultDisplay = styled.div<{ $isDark: boolean }>` - background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')}; - border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')}; - border-radius: 6px; - padding: 8px; - font-size: 11px; - max-height: 120px; - overflow-y: auto; - - pre { - margin: 0; - white-space: pre-wrap; - word-break: break-all; - color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')}; - font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; - } -` - -export default CacheAdvancedTests diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx deleted file mode 100644 index ad69fa365d..0000000000 --- a/src/renderer/src/windows/dataRefactorTest/components/CacheBasicTests.tsx +++ /dev/null @@ -1,432 +0,0 @@ -import { useCache, usePersistCache, useSharedCache } from '@renderer/data/hooks/useCache' -import { usePreference } from '@renderer/data/hooks/usePreference' -import { loggerService } from '@renderer/services/LoggerService' -import type { RendererPersistCacheKey } from '@shared/data/cache/cacheSchemas' -import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Card, Col, Divider, Input, message, Row, Select, Slider, Space, Typography } from 'antd' -import { Database, Edit, Eye, HardDrive, RefreshCw, Users, Zap } from 'lucide-react' -import React, { useRef, useState } from 'react' -import styled from 'styled-components' - -const { Text } = Typography -const { Option } = Select - -const logger = loggerService.withContext('CacheBasicTests') - -/** - * Basic cache hooks testing component - * Tests useCache, useSharedCache, and usePersistCache hooks - */ -const CacheBasicTests: React.FC = () => { - const [currentTheme] = usePreference('ui.theme_mode') - const isDarkTheme = currentTheme === ThemeMode.dark - - // useCache testing - const [memoryCacheKey, setMemoryCacheKey] = useState('test-hook-memory-1') - const [memoryCacheDefault, setMemoryCacheDefault] = useState('default-memory-value') - const [newMemoryValue, setNewMemoryValue] = useState('') - const [memoryValue, setMemoryValue] = useCache(memoryCacheKey as any, memoryCacheDefault) - - // useSharedCache testing - const [sharedCacheKey, setSharedCacheKey] = useState('test-hook-shared-1') - const [sharedCacheDefault, setSharedCacheDefault] = useState('default-shared-value') - const [newSharedValue, setNewSharedValue] = useState('') - const [sharedValue, setSharedValue] = useSharedCache(sharedCacheKey as any, sharedCacheDefault) - - // usePersistCache testing - const [persistCacheKey, setPersistCacheKey] = useState('example-1') - const [newPersistValue, setNewPersistValue] = useState('') - const [persistValue, setPersistValue] = usePersistCache(persistCacheKey) - - // Testing different data types - const [numberKey] = useState('test-number-cache' as const) - const [numberValue, setNumberValue] = useCache(numberKey as any, 42) - - const [objectKey] = useState('test-object-cache' as const) - const [objectValue, setObjectValue] = useCache(objectKey as any, { name: 'test', count: 0, active: true }) - - // Stats - const renderCountRef = useRef(0) - const [displayRenderCount, setDisplayRenderCount] = useState(0) - const [updateCount, setUpdateCount] = useState(0) - - // Available persist keys - const persistKeys: RendererPersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] - - // Update render count without causing re-renders - renderCountRef.current += 1 - - const parseValue = (value: string): any => { - if (!value) return undefined - try { - return JSON.parse(value) - } catch { - return value - } - } - - const formatValue = (value: any): string => { - if (value === undefined) return 'undefined' - if (value === null) return 'null' - if (typeof value === 'string') return `"${value}"` - return JSON.stringify(value, null, 2) - } - - // Memory cache operations - const handleMemoryUpdate = () => { - try { - const parsed = parseValue(newMemoryValue) - setMemoryValue(parsed) - setNewMemoryValue('') - setUpdateCount((prev) => prev + 1) - message.success(`Memory cache updated: ${memoryCacheKey}`) - logger.info('Memory cache updated via hook', { key: memoryCacheKey, value: parsed }) - } catch (error) { - message.error(`Memory cache update failed: ${(error as Error).message}`) - } - } - - // Shared cache operations - const handleSharedUpdate = () => { - try { - const parsed = parseValue(newSharedValue) - setSharedValue(parsed) - setNewSharedValue('') - setUpdateCount((prev) => prev + 1) - message.success(`Shared cache updated: ${sharedCacheKey} (broadcasted to other windows)`) - logger.info('Shared cache updated via hook', { key: sharedCacheKey, value: parsed }) - } catch (error) { - message.error(`Shared cache update failed: ${(error as Error).message}`) - } - } - - // Persist cache operations - const handlePersistUpdate = () => { - try { - let parsed: any - // Handle different types based on schema - if (persistCacheKey === 'example-1') { - parsed = newPersistValue // string - } else if (persistCacheKey === 'example-2') { - parsed = parseInt(newPersistValue) || 0 // number - } else if (persistCacheKey === 'example-3') { - parsed = newPersistValue === 'true' // boolean - } else if (persistCacheKey === 'example-4') { - parsed = parseValue(newPersistValue) // object - } - - setPersistValue(parsed as any) - setNewPersistValue('') - setUpdateCount((prev) => prev + 1) - message.success(`Persist cache updated: ${persistCacheKey} (saved + broadcasted)`) - logger.info('Persist cache updated via hook', { key: persistCacheKey, value: parsed }) - } catch (error) { - message.error(`Persist cache update failed: ${(error as Error).message}`) - } - } - - // Test different data types - const handleNumberUpdate = (newValue: number) => { - setNumberValue(newValue) - setUpdateCount((prev) => prev + 1) - logger.info('Number cache updated', { value: newValue }) - } - - const handleObjectUpdate = (field: string, value: any) => { - const currentValue = objectValue || { name: 'test', count: 0, active: true } - setObjectValue({ ...currentValue, [field]: value }) - setUpdateCount((prev) => prev + 1) - logger.info('Object cache updated', { field, value }) - } - - return ( - - -
- - - React Hook Tests • Renders: {displayRenderCount || renderCountRef.current} • Updates: {updateCount} - - - -
- - - {/* useCache Testing */} - - - - useCache Hook -
- } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - setMemoryCacheKey(e.target.value)} - prefix={} - /> - - setMemoryCacheDefault(e.target.value)} - prefix={} - /> - - setNewMemoryValue(e.target.value)} - onPressEnter={handleMemoryUpdate} - prefix={} - /> - - - - - Current Value: -
{formatValue(memoryValue)}
-
-
- - - - {/* useSharedCache Testing */} - - - - useSharedCache Hook - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - setSharedCacheKey(e.target.value)} - prefix={} - /> - - setSharedCacheDefault(e.target.value)} - prefix={} - /> - - setNewSharedValue(e.target.value)} - onPressEnter={handleSharedUpdate} - prefix={} - /> - - - - - Current Value: -
{formatValue(sharedValue)}
-
-
-
- - - {/* usePersistCache Testing */} - - - - usePersistCache Hook - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - - setNewPersistValue(e.target.value)} - onPressEnter={handlePersistUpdate} - prefix={} - /> - - - - - Current Value: -
{formatValue(persistValue)}
-
-
-
- - - - - - {/* Data Type Testing */} - - - - - Number Type Testing - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Key: {numberKey} - - - Current Value: {numberValue} - - - - - - - - - - - - - - - - Object Type Testing - - } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - - Key: {objectKey} - - - - handleObjectUpdate('name', e.target.value)} - style={{ width: 120 }} - /> - handleObjectUpdate('count', parseInt(e.target.value) || 0)} - style={{ width: 80 }} - /> - - - - -
{formatValue(objectValue)}
-
-
-
- -
- -
- - 💡 提示: useCache 仅在当前窗口有效 • useSharedCache 跨窗口实时同步 • usePersistCache 类型安全的持久化存储 - -
- -
- ) -} - -const TestContainer = styled.div<{ $isDark: boolean }>` - color: ${(props) => (props.$isDark ? '#fff' : '#000')}; -` - -const ResultDisplay = styled.div<{ $isDark: boolean }>` - background: ${(props) => (props.$isDark ? '#0d1117' : '#f6f8fa')}; - border: 1px solid ${(props) => (props.$isDark ? '#30363d' : '#d0d7de')}; - border-radius: 6px; - padding: 8px; - font-size: 11px; - max-height: 100px; - overflow-y: auto; - - pre { - margin: 0; - white-space: pre-wrap; - word-break: break-all; - color: ${(props) => (props.$isDark ? '#e6edf3' : '#1f2328')}; - font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; - } -` - -export default CacheBasicTests diff --git a/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx b/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx deleted file mode 100644 index ab4f25e34c..0000000000 --- a/src/renderer/src/windows/dataRefactorTest/components/CacheServiceTests.tsx +++ /dev/null @@ -1,439 +0,0 @@ -import { cacheService } from '@renderer/data/CacheService' -import { usePreference } from '@renderer/data/hooks/usePreference' -import { loggerService } from '@renderer/services/LoggerService' -import type { RendererPersistCacheKey, RendererPersistCacheSchema } from '@shared/data/cache/cacheSchemas' -import { ThemeMode } from '@shared/data/preference/preferenceTypes' -import { Button, Card, Col, Divider, Input, message, Row, Select, Space, Typography } from 'antd' -import { Clock, Database, Edit, Eye, Trash2, Zap } from 'lucide-react' -import React, { useEffect, useState } from 'react' -import styled from 'styled-components' - -const { Text } = Typography -const { Option } = Select -const { TextArea } = Input - -const logger = loggerService.withContext('CacheServiceTests') - -/** - * Direct CacheService API testing component - * Tests memory, shared, and persist cache operations - */ -const CacheServiceTests: React.FC = () => { - const [currentTheme] = usePreference('ui.theme_mode') - const isDarkTheme = currentTheme === ThemeMode.dark - - // State for test operations - const [memoryKey, setMemoryKey] = useState('test-memory-1') - const [memoryValue, setMemoryValue] = useState('{"type": "memory", "data": "test"}') - const [memoryTTL, setMemoryTTL] = useState('5000') - - const [sharedKey, setSharedKey] = useState('test-shared-1') - const [sharedValue, setSharedValue] = useState('{"type": "shared", "data": "cross-window"}') - const [sharedTTL, setSharedTTL] = useState('10000') - - const [persistKey, setPersistKey] = useState('example-1') - const [persistValue, setPersistValue] = useState('updated-example-value') - - // Display states - const [memoryResult, setMemoryResult] = useState(null) - const [sharedResult, setSharedResult] = useState(null) - const [persistResult, setPersistResult] = useState(null) - - const [updateCount, setUpdateCount] = useState(0) - - // Available persist keys from schema - const persistKeys: RendererPersistCacheKey[] = ['example-1', 'example-2', 'example-3', 'example-4'] - - const parseValue = (value: string): any => { - if (!value) return undefined - try { - return JSON.parse(value) - } catch { - return value // Return as string if not valid JSON - } - } - - const formatValue = (value: any): string => { - if (value === undefined) return 'undefined' - if (value === null) return 'null' - if (typeof value === 'string') return `"${value}"` - return JSON.stringify(value, null, 2) - } - - // Memory Cache Operations - const handleMemorySet = () => { - try { - const parsed = parseValue(memoryValue) - const ttl = memoryTTL ? parseInt(memoryTTL) : undefined - cacheService.set(memoryKey, parsed, ttl) - message.success(`Memory cache set: ${memoryKey}`) - setUpdateCount((prev) => prev + 1) - logger.info('Memory cache set', { key: memoryKey, value: parsed, ttl }) - } catch (error) { - message.error(`Memory cache set failed: ${(error as Error).message}`) - logger.error('Memory cache set failed', error as Error) - } - } - - const handleMemoryGet = () => { - try { - const result = cacheService.get(memoryKey) - setMemoryResult(result) - message.info(`Memory cache get: ${memoryKey}`) - logger.info('Memory cache get', { key: memoryKey, result }) - } catch (error) { - message.error(`Memory cache get failed: ${(error as Error).message}`) - logger.error('Memory cache get failed', error as Error) - } - } - - const handleMemoryHas = () => { - try { - const exists = cacheService.has(memoryKey) - message.info(`Memory cache has ${memoryKey}: ${exists}`) - logger.info('Memory cache has', { key: memoryKey, exists }) - } catch (error) { - message.error(`Memory cache has failed: ${(error as Error).message}`) - } - } - - const handleMemoryDelete = () => { - try { - const deleted = cacheService.delete(memoryKey) - message.info(`Memory cache delete ${memoryKey}: ${deleted}`) - setMemoryResult(undefined) - logger.info('Memory cache delete', { key: memoryKey, deleted }) - } catch (error) { - message.error(`Memory cache delete failed: ${(error as Error).message}`) - } - } - - // Shared Cache Operations - const handleSharedSet = () => { - try { - const parsed = parseValue(sharedValue) - const ttl = sharedTTL ? parseInt(sharedTTL) : undefined - cacheService.setShared(sharedKey, parsed, ttl) - message.success(`Shared cache set: ${sharedKey} (broadcasted to other windows)`) - setUpdateCount((prev) => prev + 1) - logger.info('Shared cache set', { key: sharedKey, value: parsed, ttl }) - } catch (error) { - message.error(`Shared cache set failed: ${(error as Error).message}`) - logger.error('Shared cache set failed', error as Error) - } - } - - const handleSharedGet = () => { - try { - const result = cacheService.getShared(sharedKey) - setSharedResult(result) - message.info(`Shared cache get: ${sharedKey}`) - logger.info('Shared cache get', { key: sharedKey, result }) - } catch (error) { - message.error(`Shared cache get failed: ${(error as Error).message}`) - logger.error('Shared cache get failed', error as Error) - } - } - - const handleSharedHas = () => { - try { - const exists = cacheService.hasShared(sharedKey) - message.info(`Shared cache has ${sharedKey}: ${exists}`) - logger.info('Shared cache has', { key: sharedKey, exists }) - } catch (error) { - message.error(`Shared cache has failed: ${(error as Error).message}`) - } - } - - const handleSharedDelete = () => { - try { - const deleted = cacheService.deleteShared(sharedKey) - message.info(`Shared cache delete ${sharedKey}: ${deleted} (broadcasted to other windows)`) - setSharedResult(undefined) - logger.info('Shared cache delete', { key: sharedKey, deleted }) - } catch (error) { - message.error(`Shared cache delete failed: ${(error as Error).message}`) - } - } - - // Persist Cache Operations - const handlePersistSet = () => { - try { - let parsed: any - // Handle different types based on the schema - if (persistKey === 'example-1') { - parsed = persistValue // string - } else if (persistKey === 'example-2') { - parsed = parseInt(persistValue) || 0 // number - } else if (persistKey === 'example-3') { - parsed = persistValue === 'true' // boolean - } else if (persistKey === 'example-4') { - parsed = parseValue(persistValue) // object - } - - cacheService.setPersist(persistKey, parsed as RendererPersistCacheSchema[typeof persistKey]) - message.success(`Persist cache set: ${persistKey} (saved to localStorage + broadcasted)`) - setUpdateCount((prev) => prev + 1) - logger.info('Persist cache set', { key: persistKey, value: parsed }) - } catch (error) { - message.error(`Persist cache set failed: ${(error as Error).message}`) - logger.error('Persist cache set failed', error as Error) - } - } - - const handlePersistGet = () => { - try { - const result = cacheService.getPersist(persistKey) - setPersistResult(result) - message.info(`Persist cache get: ${persistKey}`) - logger.info('Persist cache get', { key: persistKey, result }) - } catch (error) { - message.error(`Persist cache get failed: ${(error as Error).message}`) - logger.error('Persist cache get failed', error as Error) - } - } - - const handlePersistHas = () => { - try { - const exists = cacheService.hasPersist(persistKey) - message.info(`Persist cache has ${persistKey}: ${exists}`) - logger.info('Persist cache has', { key: persistKey, exists }) - } catch (error) { - message.error(`Persist cache has failed: ${(error as Error).message}`) - } - } - - // Auto-refresh results - useEffect(() => { - const interval = setInterval(() => { - // Auto-get current values for display - try { - const memResult = cacheService.get(memoryKey) - const sharedResult = cacheService.getShared(sharedKey) - const persistResult = cacheService.getPersist(persistKey) - - setMemoryResult(memResult) - setSharedResult(sharedResult) - setPersistResult(persistResult) - } catch (error) { - logger.error('Auto-refresh failed', error as Error) - } - }, 1000) - - return () => clearInterval(interval) - }, [memoryKey, sharedKey, persistKey]) - - return ( - - -
- 直接测试 CacheService API • Updates: {updateCount} • Auto-refresh: 1s -
- - - {/* Memory Cache Section */} - - - - Memory Cache -
- } - size="small" - style={{ - backgroundColor: isDarkTheme ? '#1f1f1f' : '#fff', - borderColor: isDarkTheme ? '#303030' : '#d9d9d9' - }}> - - setMemoryKey(e.target.value)} - prefix={} - /> - -