mirror of
https://github.com/linshenkx/prompt-optimizer.git
synced 2026-06-09 02:24:34 +08:00
7.5 KiB
7.5 KiB
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层手动序列化
// 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序列化
↑ 透明使用 ↑ 安全传输 ↑ 双重保护
实现方案:
- 在core包创建统一序列化工具
- 在所有ElectronProxy类中自动序列化
- 清理UI层的手动序列化代码
阶段3:完全透明(当前状态)
最终效果:
// 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- 移除手动序列化
代码简化效果
// 修改前:需要手动序列化
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调用使用相同的序列化策略
- 向后兼容:不影响现有功能
💡 经验总结
核心原则
- 在正确的层级解决问题 - IPC问题应该在IPC边界处理
- 对开发者透明 - 复杂性应该被架构吸收
- 渐进式改进 - 先解决问题,再优化架构
最佳实践
- 统一工具 - 避免重复代码
- 自动保护 - 减少人为错误
- 完整测试 - 确保架构变更的可靠性
避免的陷阱
- ❌ 过度工程化(如装饰器方案)
- ❌ 在错误的层级解决问题
- ❌ 忽视开发体验
这次架构演进是一个很好的例子,展示了如何通过合理的架构设计来解决技术问题,同时提升开发体验。
🔄 第三阶段:代理层职责边界优化 (2025-07)
问题发现
在解决Vue组件类型错误时,发现了一个重要的架构问题:ElectronProxy层承担了过多的数据格式转换职责。
现象:
- Web版运行正常,桌面版出现
[object Object]错误 - InputWithSelect组件期望String类型,但接收到Object类型
- 同样的代码在不同环境下表现不同
根本原因分析
- 类型定义不一致:
global.d.ts中定义fetchModelList返回string[],但实际返回ModelOption[] - 职责边界模糊:ElectronProxy既负责IPC通信,又处理复杂的数据格式转换
- 异步性放大问题:桌面版的IPC异步性暴露了Web版中被掩盖的竞态条件
Web版 vs 桌面版的关键差异:
- Web版:同步数据流,事件循环掩盖了竞态条件
- 桌面版:IPC异步通信创造了race condition,使潜在问题显现
解决方案
-
修复类型定义:
// 修复前 fetchModelList: (provider: string, customConfig?: any) => Promise<string[]>; // 修复后 fetchModelList: (provider: string, customConfig?: any) => Promise<Array<{value: string, label: string}>>; -
简化代理层:
// ElectronProxy只负责IPC通信,不做数据转换 async fetchModelList(provider: string, customConfig?: Partial<any>): Promise<ModelOption[]> { const safeCustomConfig = customConfig ? safeSerializeForIPC(customConfig) : customConfig; return this.electronAPI.llm.fetchModelList(provider, safeCustomConfig); } -
移除冗余事件:删除不必要的
@select事件处理,简化数据流
架构原则确立
- 单一职责:每层只负责自己的核心功能,代理层专注IPC通信
- 类型安全:TypeScript类型定义必须与实际实现保持严格一致
- 数据流简洁:避免不必要的中间转换层,减少出错可能性
经验总结
- 异步性是双刃剑:它会放大架构设计中的潜在问题
- 类型安全的重要性:类型定义不仅是文档,更是架构约束
- 职责边界要清晰:特别是在跨进程通信的场景下
- 环境差异要重视:同样的代码在不同环境下可能表现不同
这次经验强化了我们对IPC架构设计的理解,为未来的跨进程功能开发提供了重要指导。