Back to Prompt Optimizer

IPC序列化架构演进

docs/archives/115-ipc-serialization-fixes/architecture-evolution.md

2.10.27.5 KB
Original Source

IPC序列化架构演进

📋 概述

本文档记录了Electron IPC序列化处理从UI层手动处理到ElectronProxy层自动处理的架构演进过程。

🔄 演进历程

阶段1:问题发现(112-desktop-ipc-fixes)

问题:Vue响应式对象无法通过Electron IPC传递

TemplateManager.vue:1068 保存提示词失败: Error: An object could not be cloned.
ModelManager.vue:1023 添加模型失败: Error: An object could not be cloned.

解决方案:在UI层手动序列化

javascript
// UI层手动序列化
import { createSafeModelConfig } from '../utils/ipc-serialization'
const config = createSafeModelConfig(formData.value)
await modelManager.addModel(key, config)

问题

  • 需要在每个Vue组件中手动序列化
  • 容易遗漏,维护成本高
  • 开发者心智负担重

阶段2:架构优化(115-ipc-serialization-fixes)

改进思路:将序列化处理移到ElectronProxy层

新架构

Vue组件 → ElectronProxy自动序列化 → IPC → Main.js序列化
        ↑ 透明使用              ↑ 安全传输  ↑ 双重保护

实现方案

  1. 在core包创建统一序列化工具
  2. 在所有ElectronProxy类中自动序列化
  3. 清理UI层的手动序列化代码

阶段3:完全透明(当前状态)

最终效果

javascript
// Vue组件中直接使用,无需关心序列化
await modelManager.addModel(key, {
  llmParams: formData.value.llmParams // 自动序列化
})

🏗️ 架构对比

修改前:UI层手动序列化

┌─────────────┐    手动序列化    ┌──────────────┐    IPC    ┌─────────────┐
│ Vue组件     │ ──────────────→ │ ElectronProxy│ ────────→ │ Main进程    │
│ (需要手动)  │                 │ (透传)       │           │ (双重保护)  │
└─────────────┘                 └──────────────┘           └─────────────┘

问题

  • ❌ 开发者需要记住序列化
  • ❌ 容易遗漏,出错率高
  • ❌ 代码重复,维护困难

修改后:ElectronProxy层自动序列化

┌─────────────┐    直接传递     ┌──────────────┐    IPC    ┌─────────────┐
│ Vue组件     │ ──────────────→ │ ElectronProxy│ ────────→ │ Main进程    │
│ (透明使用)  │                 │ (自动序列化) │           │ (双重保护)  │
└─────────────┘                 └──────────────┘           └─────────────┘

优势

  • ✅ 对Vue组件透明
  • ✅ 自动保护,不易遗漏
  • ✅ 集中管理,易维护
  • ✅ 代码简洁,开发体验好

📊 修改统计

删除的文件

  • packages/ui/src/utils/ipc-serialization.ts - UI层序列化工具

修改的文件

  • packages/core/src/utils/ipc-serialization.ts - 新增统一序列化工具
  • packages/core/src/services/*/electron-proxy.ts - 6个代理类自动序列化
  • packages/ui/src/components/ModelManager.vue - 移除手动序列化
  • packages/ui/src/composables/usePromptOptimizer.ts - 移除手动序列化
  • packages/ui/src/composables/usePromptHistory.ts - 移除手动序列化

代码简化效果

javascript
// 修改前:需要手动序列化
import { createSafeModelConfig } from '../utils/ipc-serialization'
const config = createSafeModelConfig({
  name: newModel.value.name,
  llmParams: newModel.value.llmParams
})
await modelManager.addModel(key, config)

// 修改后:直接使用
const config = {
  name: newModel.value.name,
  llmParams: newModel.value.llmParams
}
await modelManager.addModel(key, config) // 自动序列化

🎯 技术价值

1. 开发体验提升

  • 简化开发:Vue组件无需关心序列化细节
  • 减少错误:架构层面保证序列化,避免遗漏
  • 代码简洁:删除大量样板代码

2. 架构完善

  • 分层清晰:序列化处理在正确的层级
  • 职责明确:ElectronProxy负责IPC适配
  • 易于维护:集中管理序列化逻辑

3. 可扩展性

  • 新增功能:自动获得序列化保护
  • 统一标准:所有IPC调用使用相同的序列化策略
  • 向后兼容:不影响现有功能

💡 经验总结

核心原则

  1. 在正确的层级解决问题 - IPC问题应该在IPC边界处理
  2. 对开发者透明 - 复杂性应该被架构吸收
  3. 渐进式改进 - 先解决问题,再优化架构

最佳实践

  1. 统一工具 - 避免重复代码
  2. 自动保护 - 减少人为错误
  3. 完整测试 - 确保架构变更的可靠性

避免的陷阱

  • ❌ 过度工程化(如装饰器方案)
  • ❌ 在错误的层级解决问题
  • ❌ 忽视开发体验

这次架构演进是一个很好的例子,展示了如何通过合理的架构设计来解决技术问题,同时提升开发体验。

🔄 第三阶段:代理层职责边界优化 (2025-07)

问题发现

在解决Vue组件类型错误时,发现了一个重要的架构问题:ElectronProxy层承担了过多的数据格式转换职责。

现象

  • Web版运行正常,桌面版出现[object Object]错误
  • InputWithSelect组件期望String类型,但接收到Object类型
  • 同样的代码在不同环境下表现不同

根本原因分析

  1. 类型定义不一致global.d.ts中定义fetchModelList返回string[],但实际返回ModelOption[]
  2. 职责边界模糊:ElectronProxy既负责IPC通信,又处理复杂的数据格式转换
  3. 异步性放大问题:桌面版的IPC异步性暴露了Web版中被掩盖的竞态条件

Web版 vs 桌面版的关键差异

  • Web版:同步数据流,事件循环掩盖了竞态条件
  • 桌面版:IPC异步通信创造了race condition,使潜在问题显现

解决方案

  1. 修复类型定义

    typescript
    // 修复前
    fetchModelList: (provider: string, customConfig?: any) => Promise<string[]>;
    
    // 修复后
    fetchModelList: (provider: string, customConfig?: any) => Promise<Array<{value: string, label: string}>>;
    
  2. 简化代理层

    typescript
    // ElectronProxy只负责IPC通信,不做数据转换
    async fetchModelList(provider: string, customConfig?: Partial<any>): Promise<ModelOption[]> {
      const safeCustomConfig = customConfig ? safeSerializeForIPC(customConfig) : customConfig;
      return this.electronAPI.llm.fetchModelList(provider, safeCustomConfig);
    }
    
  3. 移除冗余事件:删除不必要的@select事件处理,简化数据流

架构原则确立

  • 单一职责:每层只负责自己的核心功能,代理层专注IPC通信
  • 类型安全:TypeScript类型定义必须与实际实现保持严格一致
  • 数据流简洁:避免不必要的中间转换层,减少出错可能性

经验总结

  1. 异步性是双刃剑:它会放大架构设计中的潜在问题
  2. 类型安全的重要性:类型定义不仅是文档,更是架构约束
  3. 职责边界要清晰:特别是在跨进程通信的场景下
  4. 环境差异要重视:同样的代码在不同环境下可能表现不同

这次经验强化了我们对IPC架构设计的理解,为未来的跨进程功能开发提供了重要指导。