Files
prompt-optimizer/docs/architecture/preference-service-optimization.md
linshen 71600b5081 feat: 重构导入导出架构
## 核心架构重构
- 创建IImportExportable接口,定义统一的导入导出规范
- 新增ImportExportError错误类,提供专门的错误处理
- 将导入导出逻辑从集中式DataManager分散到各个服务
- DataManager职责精简:从375行减至67行(-308行),仅负责协调

## 存储键架构优化
- 移动storage-keys.ts从ui包到core包,实现统一管理
- 修复存储键双重用途问题:物理存储键vs逻辑JSON导出键
- PreferenceService添加'pref:'前缀处理物理存储
- 解决数据导出不完整的关键架构缺陷

## 服务层改造
### ModelManager
- 实现IImportExportable接口(+209行,-153行)
- 添加exportData/importData/validateData方法
- 保持向后兼容的数据格式

### TemplateManager
- 实现分布式导入导出逻辑
- 移除过度设计的configurable storageKey
- 统一使用PreferenceService管理用户偏好

### HistoryManager
- 添加完整的导入导出实现
- 支持数据验证和错误处理

### PreferenceService
- 实现统一的用户设置导入导出
- 处理存储键前缀转换逻辑
- 支持builtin-template-language等核心设置

## Electron桌面端更新
- main.js: 新增148行IPC处理逻辑
- preload.js: 新增177行API暴露
- 更新所有service proxy类支持新接口
- 保持IPC通信的类型安全

## 测试体系完善
- 新增各服务专门的import-export.test.ts文件
- 创建data/import-export-integration.test.ts集成测试
- 建立AI自动化测试框架验证存储键一致性
- 更新现有测试适配新架构

## 文档与架构说明
- 创建import-export-interface-design.md设计文档
- 添加storage-key-architecture.md架构说明
- 建立AI自动化测试文档体系
- 更新workspace文档记录重构过程

BREAKING CHANGE: 导入导出接口从集中式DataManager重构为分布式服务实现,
各服务现在必须实现IImportExportable接口,存储键架构发生变化
2025-07-12 11:56:14 +08:00

6.4 KiB
Raw Permalink Blame History

PreferenceService架构优化

📋 优化背景

在存储键架构重构过程中,发现了一个重要的架构不一致性问题:

问题描述

用户提出了一个关键问题:"为什么exportAllData的时候要对preferenceService特别处理呢preferenceService直接提供一个获取所有数据的接口不就好了其他几个manager都是这样的"

架构不一致性分析

其他Manager的统一模式

// 所有其他服务都提供批量获取接口
const models = await this.modelManager.getAllModels();
const userTemplates = await this.templateManager.listTemplates();
const history = await this.historyManager.getAllRecords();

PreferenceService的特殊处理问题

// ❌ 原有的特殊处理方式
for (const key of PREFERENCE_BASED_KEYS) {
  const value = await this.preferenceService.get(key, null);
  if (value !== null) {
    userSettings[key] = String(value);
  }
}

🎯 优化方案

1. 添加批量获取接口

为PreferenceService添加getAll()方法保持与其他Manager的接口一致性

export interface IPreferenceService {
  // 现有方法...
  
  /**
   * 获取所有偏好设置
   * @returns 包含所有偏好设置的键值对对象
   */
  getAll(): Promise<Record<string, string>>;
}

2. 实现批量获取逻辑

async getAll(): Promise<Record<string, string>> {
  try {
    const allKeys = await this.keys();
    const result: Record<string, string> = {};
    
    for (const key of allKeys) {
      try {
        const value = await this.get(key, null);
        if (value !== null) {
          result[key] = String(value);
        }
      } catch (error) {
        console.warn(`Failed to get preference for key "${key}":`, error);
        // 继续处理其他键,不因单个键失败而中断
      }
    }
    
    return result;
  } catch (error) {
    console.error('Error getting all preferences:', error);
    throw new Error(`Failed to get all preferences: ${error}`);
  }
}

3. 简化DataManager导出逻辑

// ✅ 优化后的统一处理方式
async exportAllData(): Promise<ExportData> {
  // 获取所有偏好设置(统一接口)
  const userSettings = await this.preferenceService.getAll();
  
  // 获取其他数据(统一接口)
  const models = await this.modelManager.getAllModels();
  const userTemplates = await this.templateManager.listTemplates();
  const history = await this.historyManager.getAllRecords();
  
  return {
    version: 1,
    data: { userSettings, models, userTemplates, history }
  };
}

📊 优化效果

架构一致性

所有服务现在都遵循相同的接口模式:

服务 批量获取方法 返回类型
ModelManager getAllModels() ModelConfig[]
TemplateManager listTemplates() Template[]
HistoryManager getAllRecords() PromptRecord[]
PreferenceService getAll() Record<string, string>

代码简化

  • 移除了存储键分类常量 - 不再需要PREFERENCE_BASED_KEYSDIRECT_STORAGE_KEYS
  • 简化了DataManager逻辑 - 从复杂的分类处理变为统一的批量调用
  • 减少了维护成本 - 新增偏好设置不需要更新DataManager

性能提升

  • 减少异步调用次数 - 从多次get()调用变为一次getAll()调用
  • 批量处理更高效 - 一次性获取所有数据,减少存储访问次数
  • 错误处理更健壮 - 单个键失败不影响其他键的获取

🔧 实现细节

错误处理策略

// 健壮的错误处理:单个键失败不影响整体
for (const key of allKeys) {
  try {
    const value = await this.get(key, null);
    if (value !== null) {
      result[key] = String(value);
    }
  } catch (error) {
    console.warn(`Failed to get preference for key "${key}":`, error);
    // 继续处理其他键
  }
}

数据类型统一

// 所有值都转换为字符串保持JSON导出的一致性
result[key] = String(value);

前缀处理透明化

  • getAll()返回的键名是原始键名(不带pref:前缀)
  • 内部前缀处理对调用者完全透明
  • 保持了PreferenceService的封装性

🧪 测试覆盖

为新的getAll()方法添加了完整的测试覆盖:

describe('批量操作', () => {
  it('should get all preferences', async () => {
    await preferenceService.set('app:settings:ui:theme-id', 'dark');
    await preferenceService.set('app:settings:ui:preferred-language', 'zh-CN');
    
    const allPreferences = await preferenceService.getAll();
    
    expect(allPreferences).toEqual({
      'app:settings:ui:theme-id': 'dark',
      'app:settings:ui:preferred-language': 'zh-CN'
    });
  });

  it('should handle errors gracefully in getAll', async () => {
    // 测试错误处理逻辑
  });
});

🚀 最佳实践总结

1. 接口一致性原则

  • 同类型服务应提供一致的接口模式
  • 批量操作比逐个操作更高效和简洁
  • 避免在上层代码中进行特殊处理

2. 错误处理策略

  • 批量操作中单个项目失败不应影响整体
  • 提供详细的错误日志便于调试
  • 保持操作的原子性和一致性

3. 封装性设计

  • 内部实现细节(如前缀)对外部透明
  • 接口设计应符合调用者的期望
  • 保持向后兼容性

📝 相关文件

修改的文件

  • packages/core/src/services/preference/types.ts - 添加getAll接口
  • packages/core/src/services/preference/service.ts - 实现getAll方法
  • packages/core/src/services/data/manager.ts - 简化导出逻辑
  • packages/core/tests/unit/preference/service.test.ts - 新增测试文件

移除的复杂性

  • 删除了PREFERENCE_BASED_KEYSDIRECT_STORAGE_KEYS常量
  • 简化了DataManager的存储键分类逻辑
  • 统一了导入导出的处理方式

🎉 总结

这次优化体现了**"保持架构一致性"**的重要性:

  1. 识别不一致性 - 用户的观察非常准确,指出了架构问题
  2. 统一接口模式 - 所有Manager都提供批量获取接口
  3. 简化上层逻辑 - DataManager不再需要特殊处理
  4. 提升性能和可维护性 - 更少的代码,更好的性能

这是一个很好的例子,说明了用户反馈如何推动架构改进,以及简单一致的设计比复杂特殊处理更优雅