/** * Стратегии миграции конфигураций виджета чата */ import { DEFAULT_CONFIG } from '../default-config' import { MigrationStrategy, MigrationContext, MigrationStepResult, ConfigV1 } from './types' /** Базовый класс для стратегий миграции */ export abstract class BaseMigrationStrategy implements MigrationStrategy { abstract name: string abstract description: string abstract appliesTo: { from: any; to: any } canApply(context: MigrationContext): boolean { return context.fromVersion === this.appliesTo.from && context.toVersion === this.appliesTo.to } abstract apply(context: MigrationContext): MigrationStepResult rollback?(_context: MigrationContext): MigrationStepResult { return { success: false, errors: [`Rollback not implemented for strategy: ${this.name}`], warnings: [], modified: false } } protected createSuccessResult(data: any, warnings: string[] = []): MigrationStepResult { return { success: true, data, errors: [], warnings, modified: true } } protected createErrorResult(errors: string[], data?: any): MigrationStepResult { return { success: false, data, errors, warnings: [], modified: false } } } /** Стратегия добавления новых полей в settings для V1->V2 */ export class AddSettingsFieldsV1toV2Strategy extends BaseMigrationStrategy { name = 'AddSettingsFieldsV1toV2' description = 'Добавляет новые поля loader, buttonStyle, buttonType в settings' appliesTo = { from: '1.0' as const, to: '2.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } as ConfigV1 const warnings: string[] = [] // Добавляем новые поля в settings const newSettings = { ...config.settings, loader: DEFAULT_CONFIG.settings.loader, // 'dots' } warnings.push(`Добавлены поля loader: '${newSettings.loader}'`) return this.createSuccessResult( { ...config, settings: newSettings }, warnings ) } catch (error) { return this.createErrorResult([`Ошибка при добавлении полей settings: ${error}`]) } } } /** Стратегия добавления chipStyle в top.params для V1->V2 */ export class AddChipStyleV1toV2Strategy extends BaseMigrationStrategy { name = 'AddChipStyleV1toV2' description = 'Добавляет поле chipStyle в sections.top.params' appliesTo = { from: '1.0' as const, to: '2.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] // Добавляем chipStyle в top.params config.sections.top.params = { ...config.sections.top.params, chipStyle: DEFAULT_CONFIG.sections.top.params.chipStyle // 'filled' } warnings.push(`Добавлено поле chipStyle: '${DEFAULT_CONFIG.sections.top.params.chipStyle}' в sections.top.params`) return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении chipStyle: ${error}`]) } } } /** Стратегия добавления bgType для сообщений в V1->V2 */ export class AddMessageBgTypeV1toV2Strategy extends BaseMigrationStrategy { name = 'AddMessageBgTypeV1toV2' description = 'Добавляет поле bgType в messageUser и messageBot' appliesTo = { from: '1.0' as const, to: '2.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] // Добавляем bgType в messageUser и messageBot config.sections.inside.messageUser = { ...config.sections.inside.messageUser, bgType: DEFAULT_CONFIG.sections.inside.messageUser.bgType // 'bubble' } config.sections.inside.messageBot = { ...config.sections.inside.messageBot, bgType: DEFAULT_CONFIG.sections.inside.messageBot.bgType // 'plain' } warnings.push('Добавлены поля bgType для messageUser и messageBot') return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении bgType для сообщений: ${error}`]) } } } /** Стратегия добавления новых кнопок approve/reject в V1->V2 */ export class AddActionButtonsV1toV2Strategy extends BaseMigrationStrategy { name = 'AddActionButtonsV1toV2' description = 'Добавляет кнопки aprooveButton и rejectButton в inside секцию' appliesTo = { from: '1.0' as const, to: '2.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] // Добавляем новые кнопки config.sections.inside = { ...config.sections.inside } warnings.push('Добавлены кнопки aprooveButton и rejectButton') return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении кнопок действий: ${error}`]) } } } /** Стратегия расширения inputSend для V1->V2 */ export class EnhanceInputSendV1toV2Strategy extends BaseMigrationStrategy { name = 'EnhanceInputSendV1toV2' description = 'Добавляет borderStyle, inputStyle, bgType в inputSend' appliesTo = { from: '1.0' as const, to: '2.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] // Расширяем inputSend config.sections.bottom.inputSend = { ...config.sections.bottom.inputSend, borderStyle: { borderColor: DEFAULT_CONFIG.sections.bottom.inputSend.borderColor, borderWidth: DEFAULT_CONFIG.sections.bottom.inputSend.borderWidth, }, inputStyle: DEFAULT_CONFIG.sections.bottom.inputSend.inputStyle, // 'inside' bgType: DEFAULT_CONFIG.sections.bottom.inputSend.bgType // 'plain' } warnings.push('Расширен inputSend с новыми полями borderStyle, inputStyle, bgType') return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при расширении inputSend: ${error}`]) } } } /** Стратегия добавления warningAlert и disclaimer в V1->V2 */ export class AddBottomElementsV1toV2Strategy extends BaseMigrationStrategy { name = 'AddBottomElementsV1toV2' description = 'Добавляет warningAlert и disclaimer в bottom секцию' appliesTo = { from: '1.0' as const, to: '2.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] // Добавляем новые элементы в bottom config.sections.bottom = { ...config.sections.bottom, // warningAlert: DEFAULT_CONFIG.sections.bottom.warningAlert } warnings.push('Добавлены warningAlert и disclaimer в bottom секцию') return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении элементов bottom: ${error}`]) } } } /** Реестр всех стратегий миграции V1->V2 */ export const V1_TO_V2_STRATEGIES: MigrationStrategy[] = [ new AddSettingsFieldsV1toV2Strategy(), new AddChipStyleV1toV2Strategy(), new AddMessageBgTypeV1toV2Strategy(), new AddActionButtonsV1toV2Strategy(), new EnhanceInputSendV1toV2Strategy(), new AddBottomElementsV1toV2Strategy() ] /** Стратегия добавления theme и container в settings для V2->V3 */ export class AddThemeAndContainerV2toV3Strategy extends BaseMigrationStrategy { name = 'AddThemeAndContainerV2toV3' description = 'Adds theme and container (innerBorder, outerBorder, gradient) to settings' appliesTo = { from: '2.0' as const, to: '3.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] // Добавляем theme если его нет if (!config.settings.theme) { config.settings.theme = DEFAULT_CONFIG.settings.theme // 'auto' warnings.push(`Добавлено поле theme: '${config.settings.theme}'`) } // Добавляем container если его нет if (!config.settings.container) { config.settings.container = { innerBorder: { width: DEFAULT_CONFIG.settings.container?.innerBorder?.width || 0, color: DEFAULT_CONFIG.settings.container?.innerBorder?.color || 'transparent', style: DEFAULT_CONFIG.settings.container?.innerBorder?.style || 'solid', }, outerBorder: { width: DEFAULT_CONFIG.settings.container?.outerBorder?.width || 0, color: DEFAULT_CONFIG.settings.container?.outerBorder?.color || 'transparent', style: DEFAULT_CONFIG.settings.container?.outerBorder?.style || 'solid', }, gradient: DEFAULT_CONFIG.settings.container?.gradient || 'transparent', } warnings.push('Added container object with innerBorder, outerBorder, gradient') } // Конвертируем borderRadius из строки в число если нужно if (typeof config.settings.borderRadius === 'string') { const borderRadiusMap: Record = { '0': 0, 'sm': 4, 'md': 8, 'lg': 12, } config.settings.borderRadius = borderRadiusMap[config.settings.borderRadius] ?? 12 warnings.push(`Конвертирован borderRadius из строки в число: ${config.settings.borderRadius}`) } return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении theme и container: ${error}`]) } } } /** Стратегия добавления border, sideMargin, borderRadius в top.params для V2->V3 */ export class AddTopParamsV2toV3Strategy extends BaseMigrationStrategy { name = 'AddTopParamsV2toV3' description = 'Добавляет border, sideMargin, borderRadius в sections.top.params' appliesTo = { from: '2.0' as const, to: '3.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] if (!config.sections?.top?.params) { config.sections.top = config.sections.top || {} config.sections.top.params = {} } // Добавляем border если его нет if (!config.sections.top.params.border) { config.sections.top.params.border = { width: DEFAULT_CONFIG.sections.top.params.border?.width || 0, color: DEFAULT_CONFIG.sections.top.params.border?.color || 'transparent', } warnings.push('Добавлено поле border в sections.top.params') } // Добавляем sideMargin если его нет if (config.sections.top.params.sideMargin === undefined) { config.sections.top.params.sideMargin = DEFAULT_CONFIG.sections.top.params.sideMargin || 0 warnings.push(`Добавлено поле sideMargin: ${config.sections.top.params.sideMargin}`) } // Добавляем borderRadius если его нет if (config.sections.top.params.borderRadius === undefined) { config.sections.top.params.borderRadius = DEFAULT_CONFIG.sections.top.params.borderRadius || 12 warnings.push(`Добавлено поле borderRadius: ${config.sections.top.params.borderRadius}`) } return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении полей top.params: ${error}`]) } } } /** Стратегия добавления dataAction в inside для V2->V3 */ export class AddDataActionV2toV3Strategy extends BaseMigrationStrategy { name = 'AddDataActionV2toV3' description = 'Добавляет dataAction в sections.inside' appliesTo = { from: '2.0' as const, to: '3.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] if (!config.sections?.inside) { config.sections.inside = {} } // Normalize dataAction to the V3 object shape (supports legacy string configs). const defaultDataActionRaw = (DEFAULT_CONFIG as any)?.sections?.inside?.dataAction const defaultDataAction = defaultDataActionRaw && typeof defaultDataActionRaw === 'object' ? defaultDataActionRaw : { type: 'steps', headerFillColor: '#595959', headerText: '#fff', bodyFillColor: '#595959', activeStepColor: '#fff', pastStepColor: '#fff', strokeColor: '#595959', strokeWidth: 0, } const cloneDefault = () => JSON.parse(JSON.stringify(defaultDataAction)) const current = (config.sections.inside as any).dataAction if (!current) { ;(config.sections.inside as any).dataAction = cloneDefault() warnings.push(`Added dataAction object (type: '${(config.sections.inside as any).dataAction.type}')`) } else if (typeof current === 'string') { ;(config.sections.inside as any).dataAction = { ...cloneDefault(), type: current, } warnings.push(`Upgraded dataAction from string to object (type: '${current}')`) } else if (typeof current === 'object') { // Fill missing fields from defaults, but keep user's existing values. ;(config.sections.inside as any).dataAction = { ...cloneDefault(), ...current, } if (!(config.sections.inside as any).dataAction.type) { ;(config.sections.inside as any).dataAction.type = cloneDefault().type warnings.push(`Filled missing dataAction.type with default ('${(config.sections.inside as any).dataAction.type}')`) } } return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении dataAction: ${error}`]) } } } /** Стратегия расширения welcomeMessage для V2->V3 */ export class EnhanceWelcomeMessageV2toV3Strategy extends BaseMigrationStrategy { name = 'EnhanceWelcomeMessageV2toV3' description = 'Расширяет welcomeMessage с borderColor и другими полями' appliesTo = { from: '2.0' as const, to: '3.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] if (!config.sections?.inside?.welcomeMessage) { config.sections.inside.welcomeMessage = {} } const welcomeMsg = config.sections.inside.welcomeMessage const defaultWelcome = DEFAULT_CONFIG.sections.inside.welcomeMessage // Добавляем недостающие поля if (!welcomeMsg.borderColor && defaultWelcome.borderColor) { welcomeMsg.borderColor = defaultWelcome.borderColor warnings.push(`Добавлено поле borderColor для welcomeMessage`) } if (!welcomeMsg.bgType && defaultWelcome.bgType) { welcomeMsg.bgType = defaultWelcome.bgType warnings.push(`Добавлено поле bgType для welcomeMessage: '${welcomeMsg.bgType}'`) } if (!welcomeMsg.bgColor && defaultWelcome.bgColor) { welcomeMsg.bgColor = defaultWelcome.bgColor warnings.push(`Добавлено поле bgColor для welcomeMessage`) } if (welcomeMsg.borderWidth === undefined && defaultWelcome.borderWidth !== undefined) { welcomeMsg.borderWidth = defaultWelcome.borderWidth warnings.push(`Добавлено поле borderWidth для welcomeMessage`) } if (!welcomeMsg.fontSize && defaultWelcome.fontSize) { welcomeMsg.fontSize = defaultWelcome.fontSize warnings.push(`Добавлено поле fontSize для welcomeMessage`) } if (!welcomeMsg.size && defaultWelcome.size) { welcomeMsg.size = defaultWelcome.size warnings.push(`Добавлено поле size для welcomeMessage`) } if (!welcomeMsg.borderRadius && defaultWelcome.borderRadius) { welcomeMsg.borderRadius = defaultWelcome.borderRadius warnings.push(`Добавлено поле borderRadius для welcomeMessage`) } return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при расширении welcomeMessage: ${error}`]) } } } /** Стратегия добавления btnVoice в bottom для V2->V3 */ export class AddVoiceButtonV2toV3Strategy extends BaseMigrationStrategy { name = 'AddVoiceButtonV2toV3' description = 'Добавляет btnVoice в sections.bottom' appliesTo = { from: '2.0' as const, to: '3.0' as const } apply(context: MigrationContext): MigrationStepResult { try { const config = { ...context.config } const warnings: string[] = [] if (!config.sections?.bottom) { config.sections.bottom = {} } // Добавляем btnVoice если его нет if (!config.sections.bottom.btnVoice) { config.sections.bottom.btnVoice = { color: DEFAULT_CONFIG.sections.bottom.btnVoice?.color || '#fff', bgColor: DEFAULT_CONFIG.sections.bottom.btnVoice?.bgColor || 'rgba(255, 255, 255, 0.10)', showType: DEFAULT_CONFIG.sections.bottom.btnVoice?.showType || 'icon', borderRadius: DEFAULT_CONFIG.sections.bottom.btnVoice?.borderRadius || 50, borderColor: DEFAULT_CONFIG.sections.bottom.btnVoice?.borderColor || '#595959', borderWidth: DEFAULT_CONFIG.sections.bottom.btnVoice?.borderWidth || 1, size: DEFAULT_CONFIG.sections.bottom.btnVoice?.size || 'md', fontSize: DEFAULT_CONFIG.sections.bottom.btnVoice?.fontSize || 12, icon: DEFAULT_CONFIG.sections.bottom.btnVoice?.icon || { showIcon: true, src: '', color: '#fff', size: 18, }, } if(!config.sections.bottom.btnVoice.bgColor) { config.sections.bottom.btnVoice.bgColor = DEFAULT_CONFIG.sections.bottom.btnVoice.bgColor warnings.push(`Добавлено поле bgColor для btnVoice: '${config.sections.bottom.btnVoice.bgColor}'`) } warnings.push('Добавлен btnVoice в sections.bottom') } return this.createSuccessResult(config, warnings) } catch (error) { return this.createErrorResult([`Ошибка при добавлении btnVoice: ${error}`]) } } } /** Реестр всех стратегий миграции V2->V3 */ export const V2_TO_V3_STRATEGIES: MigrationStrategy[] = [ new AddThemeAndContainerV2toV3Strategy(), new AddTopParamsV2toV3Strategy(), new AddDataActionV2toV3Strategy(), new EnhanceWelcomeMessageV2toV3Strategy(), new AddVoiceButtonV2toV3Strategy() ] /** Получить все стратегии для конкретного перехода версий */ export function getStrategiesForMigration(from: string, to: string): MigrationStrategy[] { // Reuse V2->V3 strategies for any 3.0->3.1 hop to avoid empty step. if (from === '3.0' && to === '3.1') { return V2_TO_V3_STRATEGIES.map(strategy => { // Clone strategy to avoid mutating original appliesTo; preserve methods. const clone = Object.create(strategy) as MigrationStrategy clone.appliesTo = { from: '3.0' as const, to: '3.1' as const } return clone }) } const allStrategies = [ ...V1_TO_V2_STRATEGIES, ...V2_TO_V3_STRATEGIES ] return allStrategies.filter(strategy => strategy.appliesTo.from === from && strategy.appliesTo.to === to ) } /** Получить все доступные стратегии */ export function getAllStrategies(): MigrationStrategy[] { return [ ...V1_TO_V2_STRATEGIES, ...V2_TO_V3_STRATEGIES ] }