feat: 添加IPC架构分析文档,优化Vue响应式对象的序列化处理,修复应用退出时无限循环问题

This commit is contained in:
linshen committed 2025-07-05 17:36:51 +08:00
1 parent 8a35c010cc
commit bb2cf778d5
28 files changed
+1482 -885

No files matched your search

+19 -83
View File
@@ -1,95 +1,31 @@
# 112-desktop-ipc-fixes - 桌面端IPC修复合集
# 112-Desktop IPC修复
## 📋 项目概述
## 📋 概述
- **项目编号**: 112
- **项目名称**: 桌面端IPC修复合集
- **开发时间**: 2025-01-04 - 2025-07-03
- **项目状态**: ✅ 已完成
- **主要开发者**: AI Assistant
解决Desktop版本中的IPC相关问题,包括语言切换功能异常、IPC调用链不完整等问题。
## 🎯 项目目标
## 🎯 主要问题
### 主要目标
- 修复桌面端IPC通信相关的各种问题
- 统一Web和Electron环境的异步接口调用
- 确保跨环境架构的一致性和稳定性
### 1. 语言切换按钮显示异常
- **问题**: 显示"Object Promise"而不是正确的语言名称
- **原因**: 异步接口被当作同步值使用
- **解决**: 统一异步接口设计,完善IPC调用链
### 具体修复内容
1. **语言切换按钮修复** - 解决功能提示词管理语言切换按钮显示"Object Promise"问题
2. **IPC架构分析与修复** - 修复Desktop版本中服务未初始化和方法不存在的问题
### 2. IPC架构不完整
- **问题**: 代理类方法缺失,IPC链路不完整
- **原因**: 接口定义与实现不一致
- **解决**: 建立完整的IPC开发流程和检查清单
## ✅ 完成情况
## 📁 文档结构
### 核心功能完成情况
- [x] **语言切换按钮修复** - 100% 完成
- 语言切换按钮现在正确显示"中文"或"English"
- 完全解决了"Object Promise"显示问题
- [x] **IPC调用一致性** - 100% 完成
- 统一了Web和Electron环境的异步接口
- 修复了所有IPC调用的异步处理问题
- [x] **架构违规修复** - 100% 完成
- 修复了preload.js中的功能自实现问题
- 确保严格遵循IPC转发原则
- [x] **类型安全改进** - 100% 完成
- 统一使用接口类型,确保跨环境兼容性
- 修复了所有TypeScript类型错误
### 技术实现完成情况
- [x] **接口统一** - 创建了`ITemplateLanguageService`接口
- [x] **调用链修复** - 从UI层到主进程的完整异步调用链
- [x] **错误处理** - 应用立即错误原则,移除错误掩盖机制
- [x] **代码清理** - 删除了冗余的`getTemplatesByType`方法
- [x] **质量验证** - 所有构建和功能测试通过
## 🎉 主要成果
### 架构改进
- **统一异步接口设计** - 创建了`ITemplateLanguageService`接口,确保跨环境一致性
- **IPC架构规范化** - 修复了preload.js架构违规,确保严格遵循IPC转发原则
- **接口驱动设计** - 统一使用接口类型而非具体类,确保跨环境类型兼容性
### 稳定性提升
- **错误处理改进** - 应用立即错误原则,让错误自然传播,便于问题定位
- **类型安全保障** - 利用TypeScript确保接口实现的完整性和一致性
- **功能验证完整** - Electron应用正常启动,所有功能正常工作
### 开发体验优化
- **代码简化** - 删除了冗余的`getTemplatesByType`方法,简化了接口
- **架构清晰** - 建立了清晰的跨环境接口调用规范
- **经验积累** - 形成了完整的IPC开发最佳实践
## 📊 影响范围
### 修改的文件
- `packages/core/src/services/template/languageService.ts` - 接口定义和实现
- `packages/core/src/services/template/electron-language-proxy.ts` - Electron代理实现
- `packages/core/src/services/template/manager.ts` - 模板管理器实现
- `packages/ui/src/components/BuiltinTemplateLanguageSwitch.vue` - UI组件
- `packages/desktop/main.js` - 主进程IPC处理器
- `packages/desktop/preload.js` - preload脚本
- `packages/ui/src/composables/useAppInitializer.ts` - 服务初始化
- `packages/core/src/services/template/types.ts` - 类型定义
### 影响的功能
- **功能提示词管理** - 语言切换功能完全正常
- **模板管理** - 所有模板相关功能正常工作
- **跨环境兼容性** - Web和Electron环境行为一致
- **language-switch-fix.md** - 语言切换功能修复详情
- **ipc-architecture-analysis.md** - IPC架构分析和最佳实践
- **desktop-development-experience.md** - Desktop开发经验总结
## 🔗 相关文档
- [语言切换修复详解](./language-switch-fix.md) - 语言切换按钮修复的详细过程
- [IPC架构修复详解](./desktop-ipc-fix.md) - IPC架构问题分析与修复
- [技术实现详解](./implementation.md) - 详细的技术实现过程
- [开发经验总结](./experience.md) - 重要的开发经验和最佳实践
- [115-IPC序列化修复](../115-ipc-serialization-fixes/) - Vue响应式对象序列化问题的解决方案
## 🚀 后续工作
## 💡 核心价值
### 建议的改进方向
- **持续监控** - 关注类似的异步接口一致性问题
- **经验应用** - 将本次修复的经验应用到其他服务的接口设计中
- **文档完善** - 将IPC开发规范整理成开发指南
## 相关功能点
- 前置依赖:111-electron-preference-architecture
- 后续功能:113-full-service-refactoring
本目录专注于Desktop环境下的IPC架构问题,为建立完整的跨进程通信机制提供了经验和最佳实践。这些经验为后续的序列化优化(115)奠定了基础。
@@ -1,130 +0,0 @@
# Desktop IPC 修复与架构分析
## 📝 归档说明
- **任务名称**: Desktop IPC 修复与架构分析
- **完成日期**: 2025-07-03
- **核心问题**: Desktop 版本中 `BuiltinTemplateLanguageSwitch` 组件因服务未初始化和方法不存在而出错。
## 🔍 根本原因分析
### 1. 架构差异
- **Web环境(单进程)**: 可直接调用服务实例
- **Desktop环境(多进程)**: 必须通过IPC进行进程间通信
### 2. 契约缺失
- `ITemplateManager` 接口中缺少语言切换相关方法
- 导致代理类无法转发调用
- 接口定义与实际需求不匹配
### 3. 实现不完整
- ElectronTemplateManagerProxy 缺少必要的方法实现
- preload.js 和 main.js 中缺少完整的IPC通信链路
## 🛠️ 解决方案
### 1. 接口完善
在 `ITemplateManager` 接口中补充缺失的方法定义:
```typescript
export interface ITemplateManager {
// 现有方法...
// 新增语言切换相关方法
getCurrentLanguage(): Promise<BuiltinTemplateLanguage>;
setLanguage(language: BuiltinTemplateLanguage): Promise<void>;
toggleLanguage(): Promise<BuiltinTemplateLanguage>;
getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]>;
}
```
### 2. 代理类修正
修正 `ElectronTemplateManagerProxy`,使其通过IPC正确转发调用:
```typescript
export class ElectronTemplateManagerProxy implements ITemplateManager {
// 缓存同步方法的结果
private cache = new Map<string, any>();
async getCurrentLanguage(): Promise<BuiltinTemplateLanguage> {
return await this.electronAPI.getCurrentLanguage();
}
async setLanguage(language: BuiltinTemplateLanguage): Promise<void> {
await this.electronAPI.setLanguage(language);
// 清除相关缓存
this.cache.delete('currentLanguage');
}
// 其他方法实现...
}
```
### 3. IPC通信链路完善
在 `preload.js` 中添加IPC方法:
```javascript
// preload.js
const electronAPI = {
// 现有方法...
// 新增语言切换IPC方法
getCurrentLanguage: () => ipcRenderer.invoke('template-getCurrentLanguage'),
setLanguage: (language) => ipcRenderer.invoke('template-setLanguage', language),
toggleLanguage: () => ipcRenderer.invoke('template-toggleLanguage'),
getSupportedLanguages: () => ipcRenderer.invoke('template-getSupportedLanguages'),
};
```
在 `main.js` 中添加IPC处理器:
```javascript
// main.js
ipcMain.handle('template-getCurrentLanguage', async () => {
try {
const result = await templateManager.getCurrentLanguage();
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
// 其他处理器...
```
## 💡 经验总结
### 接口驱动开发
- **原则**: 所有跨模块、跨进程的调用都应依赖于接口(契约),而不是具体实现
- **好处**: 确保不同环境下的实现一致性
- **实践**: 先定义接口,再实现具体功能
### 代理模式规范
- **原则**: IPC代理类应严格遵守接口,只做转发,不实现业务逻辑
- **好处**: 保持架构清晰,避免逻辑分散
- **实践**: 代理类只负责IPC调用和错误处理
### 环境差异处理
- **原则**: 必须充分考虑Web和Desktop环境的架构差异(单/多进程)
- **避免**: 依赖特定环境的"捷径"实现
- **实践**: 设计时优先考虑最复杂环境的需求
## 🔧 架构检查清单
### IPC接口完整性
- [ ] 接口中是否定义了所有需要的方法?
- [ ] 方法签名是否与实际需求匹配?
- [ ] 是否考虑了异步调用的需求?
### 代理类实现
- [ ] 代理类是否实现了接口的所有方法?
- [ ] 是否正确处理了IPC调用的异步性?
- [ ] 是否有适当的错误处理机制?
### IPC通信链路
- [ ] preload.js中是否有对应的IPC方法?
- [ ] main.js中是否有对应的IPC处理器?
- [ ] IPC调用是否使用统一的错误处理格式?
### 环境兼容性
- [ ] Web和Desktop环境是否使用相同的接口?
- [ ] 是否避免了环境特定的实现依赖?
- [ ] 是否有适当的环境检测和适配机制?
这次修复为后续的跨环境架构设计提供了重要的经验和规范。
@@ -1,286 +0,0 @@
# 开发经验总结
## 🎯 核心经验
### 1. IPC 异步调用一致性原则
**核心发现**: Web 和 Electron 环境必须使用完全相同的异步接口签名。
**问题表现**:
- UI 显示 "Object Promise" 而不是实际值
- 异步方法被当作同步使用
- 跨环境行为不一致
**解决原则**:
```typescript
// ✅ 正确:统一的异步接口
export interface ITemplateLanguageService {
getCurrentLanguage(): Promise<BuiltinTemplateLanguage>;
getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]>;
}
// ❌ 错误:混合同步/异步接口
export interface ITemplateLanguageService {
getCurrentLanguage(): BuiltinTemplateLanguage; // 同步
getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]>; // 异步
}
```
**检查清单**:
- [ ] IPC 方法是否返回 Promise?
- [ ] 代理类方法签名是否与 IPC 方法匹配?
- [ ] 调用方是否正确使用 `await`?
- [ ] 接口定义是否反映真实的异步性质?
- [ ] Web 和 Electron 环境是否使用相同的接口?
### 2. preload.js 架构原则
**核心原则**: preload.js 严禁自己实现功能,所有方法都必须通过 IPC 调用主进程。
**错误模式**:
```javascript
// ❌ 错误:在 preload.js 中自己实现功能
isInitialized: () => {
return true; // 硬编码返回值
},
```
**正确模式**:
```javascript
// ✅ 正确:通过 IPC 调用主进程
isInitialized: async () => {
const result = await ipcRenderer.invoke('model-isInitialized');
if (!result.success) throw new Error(result.error);
return result.data;
},
```
**架构检查要点**:
- [ ] 是否所有方法都通过 `ipcRenderer.invoke()` 调用?
- [ ] 是否没有任何硬编码的返回值?
- [ ] 是否没有本地状态管理?
- [ ] 是否使用统一的错误处理格式?
- [ ] 是否与对应的 ElectronProxy 类方法签名一致?
### 3. 接口驱动设计原则
**核心原则**: 在服务注入和类型声明中必须使用接口类型,不能使用具体类类型。
**问题表现**:
```typescript
// ❌ 错误:使用具体类类型
let dataManager: DataManager;
export interface AppServices {
dataManager: DataManager;
}
```
**正确实现**:
```typescript
// ✅ 正确:使用接口类型
let dataManager: IDataManager;
export interface AppServices {
dataManager: IDataManager;
}
```
**设计优势**:
- **跨环境兼容性** - 接口确保 Web 和 Electron 环境的实现可以互换
- **代理模式支持** - ElectronProxy 类可以正确实现接口而不需要继承具体类
- **类型安全** - TypeScript 编译器确保接口实现的完整性
## 🛠️ 技术实现经验
### 1. Promise 对象显示问题诊断
**问题识别**:
当 UI 显示 "Object Promise" 时,通常表示异步方法被当作同步使用。
**诊断步骤**:
1. 检查显示异常的 UI 组件
2. 追踪数据来源的方法调用
3. 确认方法是否返回 Promise 对象
4. 检查调用方是否使用 `await`
**修复模式**:
```vue
<!-- 修复前 -->
<template>
<span>{{ currentLanguage }}</span> <!-- 显示 "Object Promise" -->
</template>
<script>
const currentLanguage = computed(() => {
return service.getCurrentLanguage(); // 返回 Promise
});
</script>
<!-- 修复后 -->
<template>
<span>{{ currentLanguage }}</span> <!-- 显示 "中文" -->
</template>
<script>
const currentLanguage = ref('');
const loadCurrentLanguage = async () => {
currentLanguage.value = await service.getCurrentLanguage(); // 正确使用 await
};
</script>
```
### 2. 接口实现完整性检查
**常见问题**: 在创建适配器时遗漏某些接口方法。
**检查方法**:
1. 利用 TypeScript 编译器检查接口实现
2. 确保适配器实现接口的所有方法
3. 验证方法签名与接口定义完全匹配
**修复示例**:
```typescript
// 修复前 - 缺失 getRecord 方法
const historyManagerAdapter: IHistoryManager = {
getRecords: () => historyManagerInstance.getRecords(),
// ❌ 缺失 getRecord 方法
addRecord: (record) => historyManagerInstance.addRecord(record),
};
// 修复后 - 完整实现
const historyManagerAdapter: IHistoryManager = {
getRecords: () => historyManagerInstance.getRecords(),
getRecord: (id) => historyManagerInstance.getRecord(id), // ✅ 添加缺失方法
addRecord: (record) => historyManagerInstance.addRecord(record),
};
```
### 3. 冗余代码清理策略
**识别标准**:
- 方法标记为 `@deprecated`
- 实现只是简单转发到其他方法
- 没有实际调用的代码
**清理步骤**:
1. 确认方法已被替代且无实际使用
2. 从接口定义中删除
3. 从所有实现类中删除
4. 从代理类和适配器中删除
5. 验证构建和功能正常
## 🚫 避坑指南
### 1. 异步接口设计陷阱
**陷阱**: 在不同环境中使用不同的方法签名(同步 vs 异步)。
**避免方法**:
- 设计接口时优先考虑最复杂环境的需求(通常是 Electron)
- 如果任何环境需要异步,所有环境都使用异步接口
- 使用 TypeScript 接口确保实现一致性
### 2. preload.js 实现陷阱
**陷阱**: 为了简化而在 preload.js 中直接返回硬编码值。
**避免方法**:
- 严格遵循"只做 IPC 转发"原则
- 所有业务逻辑都在主进程中实现
- 使用统一的错误处理格式
### 3. 类型安全陷阱
**陷阱**: 在服务注入中混合使用接口类型和具体类类型。
**避免方法**:
- 统一使用接口类型进行服务注入
- 在 AppServices 接口中只使用接口类型
- 利用 TypeScript 编译器检查类型一致性
### 4. 错误处理陷阱
**陷阱**: 使用 try-catch 掩盖错误,静默处理异常。
**避免方法**:
- 应用立即错误原则,让错误自然传播
- 只在确实需要处理的地方使用 try-catch
- 提供明确的错误信息,便于调试
## 🔄 架构设计经验
### 1. 跨环境接口设计
**设计原则**:
- **统一性优先** - 所有环境使用相同的接口
- **异步优先** - 如果任何环境需要异步,全部使用异步
- **类型安全** - 利用 TypeScript 确保实现一致性
**实现模式**:
```typescript
// 1. 定义统一接口
export interface IService {
method(): Promise<Result>;
}
// 2. Web 环境实现
export class WebService implements IService {
async method(): Promise<Result> {
// 同步逻辑包装为异步
return Promise.resolve(syncResult);
}
}
// 3. Electron 环境实现
export class ElectronServiceProxy implements IService {
async method(): Promise<Result> {
// 直接调用异步 IPC
return this.electronAPI.method();
}
}
```
### 2. 错误处理架构
**立即错误原则**:
- 错误应该在发生的地方立即抛出
- 不要使用默认值掩盖错误
- 让调用者决定如何处理错误
**实现模式**:
```typescript
// ✅ 正确:立即抛出错误
const refresh = async () => {
const service = getService();
if (!service) {
throw new Error('Service not available');
}
return await service.getData(); // 让错误自然传播
};
// ❌ 错误:掩盖错误
const refresh = async () => {
try {
const service = getService();
return await service.getData();
} catch (error) {
console.error('Error:', error);
return []; // 掩盖了问题
}
};
```
### 3. 代码清理策略
**清理时机**:
- 功能完成后立即清理
- 发现冗余代码时及时处理
- 定期审查标记为 `@deprecated` 的代码
**清理原则**:
- 确保没有实际使用
- 完整删除(接口、实现、调用)
- 验证删除后功能正常
这些经验为后续的 IPC 开发和跨环境架构设计提供了重要的参考和指导。
@@ -1,224 +0,0 @@
# 技术实现详解
## 🔧 架构设计
### 问题根因分析
**核心问题**: 功能提示词管理的语言切换按钮显示 "Object Promise" 而不是正确的语言名称。
**根本原因**: IPC 调用异步处理不一致
- **Electron 环境**: `ElectronTemplateLanguageServiceProxy` 的方法调用异步 IPC 方法,但被定义为同步
- **返回 Promise 对象**: 当 Promise 对象被用作字符串显示时,显示为 "Object Promise"
- **接口不匹配**: Web 环境和 Electron 环境的方法签名不一致
### 解决方案架构
#### 1. 统一异步接口设计
```typescript
// 创建统一的异步接口
export interface ITemplateLanguageService {
initialize(): Promise<void>;
getCurrentLanguage(): Promise<BuiltinTemplateLanguage>;
setLanguage(language: BuiltinTemplateLanguage): Promise<void>;
toggleLanguage(): Promise<BuiltinTemplateLanguage>;
isValidLanguage(language: string): Promise<boolean>;
getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]>;
getLanguageDisplayName(language: BuiltinTemplateLanguage): string;
isInitialized(): boolean;
}
```
#### 2. 跨环境实现统一
- **Web 环境**: `TemplateLanguageService` 实现异步接口
- **Electron 环境**: `ElectronTemplateLanguageServiceProxy` 实现相同的异步接口
## 🐛 问题诊断与解决
### 问题1: 语言切换按钮显示 "Object Promise"
**诊断过程**:
1. 发现 UI 组件中显示异常
2. 追踪到 `getCurrentLanguage()` 方法返回 Promise 对象
3. 确认 Electron 代理类方法签名与 IPC 异步性质不匹配
**解决方案**:
```typescript
// 修复前 - 错误的同步方法
getCurrentLanguage(): BuiltinTemplateLanguage {
return this.electronAPI.getCurrentBuiltinTemplateLanguage(); // 返回 Promise
}
// 修复后 - 正确的异步方法
async getCurrentLanguage(): Promise<BuiltinTemplateLanguage> {
return await this.electronAPI.getCurrentBuiltinTemplateLanguage();
}
```
### 问题2: preload.js 架构违规
**诊断过程**:
1. 全面检查 IPC 实现发现 `model.isInitialized()` 方法违规
2. 发现 preload.js 中硬编码返回值而非 IPC 调用
**解决方案**:
```javascript
// 修复前 - 架构违规
isInitialized: () => {
return true; // 硬编码返回值
},
// 修复后 - 正确的 IPC 调用
isInitialized: async () => {
const result = await ipcRenderer.invoke('model-isInitialized');
if (!result.success) throw new Error(result.error);
return result.data;
},
```
### 问题3: TypeScript 类型不匹配
**诊断过程**:
1. 发现 `ElectronDataManagerProxy` 类型缺少 `DataManager` 类的属性
2. 确认问题是使用具体类类型而非接口类型
**解决方案**:
```typescript
// 修复前 - 使用具体类类型
let dataManager: DataManager;
export interface AppServices {
dataManager: DataManager;
}
// 修复后 - 使用接口类型
let dataManager: IDataManager;
export interface AppServices {
dataManager: IDataManager;
}
```
## 📝 实施步骤
### 步骤1: 创建统一接口 (2025-01-04 22:00)
1. 定义 `ITemplateLanguageService` 接口
2. 将所有方法定义为异步版本
3. 确保 Web 和 Electron 环境使用相同的接口
### 步骤2: 更新实现类 (2025-01-04 22:15)
1. 修改 `TemplateLanguageService` 实现
2. 将 `getCurrentLanguage()` 和 `getSupportedLanguages()` 改为异步
3. 修复 `isValidLanguage()` 中的异步调用
### 步骤3: 更新 Electron 代理 (2025-01-04 22:30)
1. 更新 `ElectronTemplateLanguageServiceProxy`
2. 实现 `ITemplateLanguageService` 接口
3. 确保所有方法正确转发 IPC 调用
### 步骤4: 修复调用链 (2025-01-04 22:45)
1. 修复 `TemplateManager` 中的异步调用
2. 更新 `BuiltinTemplateLanguageSwitch.vue` 组件
3. 修复主进程 IPC 处理器
4. 更新接口类型定义
### 步骤5: 全面 IPC 检查 (2025-01-04 23:15)
1. 检查所有 ElectronProxy 类的方法完整性
2. 修复 preload.js 架构违规问题
3. 统一异步接口规范
### 步骤6: 类型安全修复 (2025-01-04 23:45)
1. 修复服务注入中的类型不匹配
2. 统一使用接口类型而非具体类
3. 确保跨环境类型兼容性
### 步骤7: 代码清理 (2025-01-04 24:00)
1. 删除冗余的 `getTemplatesByType` 方法
2. 简化接口设计
3. 应用立即错误原则
## 🔍 调试过程
### 调试工具和方法
1. **TypeScript 编译器** - 发现类型不匹配问题
2. **Electron 开发者工具** - 检查 IPC 调用和错误
3. **控制台日志** - 追踪异步调用流程
4. **构建验证** - 确保修复不破坏现有功能
### 关键调试发现
1. **Promise 对象显示** - 当异步方法被当作同步使用时会显示 "Object Promise"
2. **IPC 调用规范** - preload.js 严禁自己实现功能,必须通过 IPC 调用主进程
3. **接口驱动重要性** - 使用接口类型确保跨环境兼容性
## 🧪 测试验证
### 构建测试
- ✅ Core 包构建成功
- ✅ UI 包构建成功
- ✅ TypeScript 编译无错误
### 功能测试
- ✅ Electron 应用成功启动
- ✅ 语言切换按钮正确显示语言名称
- ✅ 模板加载功能正常(7个模板)
- ✅ 模型管理功能正常(5个模型)
- ✅ IPC 调用正常工作
### 回归测试
- ✅ 所有原有功能保持正常
- ✅ Web 和 Electron 环境行为一致
- ✅ 错误处理符合立即错误原则
## 🔧 技术细节
### 关键代码修改
#### 1. 接口定义统一
```typescript
// packages/core/src/services/template/languageService.ts
export interface ITemplateLanguageService {
getCurrentLanguage(): Promise<BuiltinTemplateLanguage>;
getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]>;
// ... 其他方法
}
```
#### 2. 异步实现修复
```typescript
// packages/core/src/services/template/languageService.ts
public async getCurrentLanguage(): Promise<BuiltinTemplateLanguage> {
await this.initialize();
return this.currentLanguage;
}
```
#### 3. IPC 调用修复
```javascript
// packages/desktop/preload.js
getCurrentBuiltinTemplateLanguage: async () => {
const result = await ipcRenderer.invoke('template-getCurrentBuiltinTemplateLanguage');
if (!result.success) throw new Error(result.error);
return result.data;
},
```
#### 4. UI 组件修复
```vue
<!-- packages/ui/src/components/BuiltinTemplateLanguageSwitch.vue -->
<script setup>
const currentLanguage = ref('')
const loadCurrentLanguage = async () => {
const service = getTemplateLanguageService.value
if (!service) {
throw new Error('Template language service not available')
}
currentLanguage.value = await service.getCurrentLanguage()
}
</script>
```
### 架构改进要点
1. **统一异步接口** - 确保 Web 和 Electron 环境使用相同的异步接口签名
2. **严格 IPC 转发** - preload.js 只做转发,不实现任何业务逻辑
3. **接口驱动设计** - 使用接口类型而非具体类,确保跨环境兼容
4. **立即错误原则** - 让错误自然传播,便于问题定位和调试
@@ -0,0 +1,172 @@
# IPC架构分析与开发经验
## 📝 背景
在Desktop版本开发中遇到的IPC架构问题分析和解决经验。
## 🔍 架构差异分析
### 1. Web环境 vs Desktop环境
**Web环境(单进程)**:
```
Vue组件 → 直接调用 → 服务实例
```
**Desktop环境(多进程)**:
```
Vue组件 → ElectronProxy → IPC → Main进程 → 服务实例
```
### 2. 常见问题模式
#### 问题1:接口契约缺失
```typescript
// ❌ 接口定义不完整
interface ITemplateManager {
getTemplate(id: string): Promise<Template>;
// 缺少语言相关方法
}
// ✅ 完整的接口定义
interface ITemplateManager {
getTemplate(id: string): Promise<Template>;
getCurrentBuiltinTemplateLanguage(): Promise<BuiltinTemplateLanguage>;
changeBuiltinTemplateLanguage(language: BuiltinTemplateLanguage): Promise<void>;
}
```
#### 问题2:代理实现不完整
```typescript
// ❌ 代理类缺少方法
class ElectronTemplateManagerProxy implements ITemplateManager {
async getTemplate(id: string): Promise<Template> {
return this.electronAPI.getTemplate(id);
}
// 缺少其他方法的实现
}
// ✅ 完整的代理实现
class ElectronTemplateManagerProxy implements ITemplateManager {
async getTemplate(id: string): Promise<Template> {
return this.electronAPI.getTemplate(id);
}
async getCurrentBuiltinTemplateLanguage(): Promise<BuiltinTemplateLanguage> {
return this.electronAPI.getCurrentBuiltinTemplateLanguage();
}
}
```
#### 问题3:IPC链路不完整
```javascript
// preload.js - 缺少方法暴露
window.electronAPI = {
template: {
getTemplate: (id) => ipcRenderer.invoke('template-getTemplate', id),
// 缺少语言相关方法
}
}
// main.js - 缺少处理器
ipcMain.handle('template-getTemplate', async (event, id) => {
// 处理逻辑
});
// 缺少语言相关处理器
```
## 🛠️ 修复策略
### 1. 接口优先设计
```typescript
// 步骤1:定义完整接口
export interface ITemplateManager {
// 所有需要的方法
}
// 步骤2:Web环境实现
export class TemplateManager implements ITemplateManager {
// 完整实现
}
// 步骤3:Electron代理实现
export class ElectronTemplateManagerProxy implements ITemplateManager {
// 完整代理实现
}
```
### 2. IPC链路完整性检查
```
Vue组件调用 → 检查代理方法 → 检查preload暴露 → 检查main处理器 → 检查服务方法
```
### 3. 错误处理原则
```typescript
// ❌ 错误掩盖
async someMethod() {
try {
return await this.service.method();
} catch (error) {
return null; // 掩盖了错误
}
}
// ✅ 错误传播
async someMethod() {
return await this.service.method(); // 让错误自然传播
}
```
## 🎯 开发检查清单
### IPC功能开发检查
- [ ] 接口定义是否完整?
- [ ] Web环境实现是否完整?
- [ ] Electron代理实现是否完整?
- [ ] preload.js是否暴露了所有方法?
- [ ] main.js是否有对应的处理器?
- [ ] 错误处理是否正确?
- [ ] 两种环境是否都测试过?
### 架构违规检查
- [ ] preload.js是否只做转发,没有业务逻辑?
- [ ] 是否所有方法都是异步的?
- [ ] 是否使用了统一的错误处理格式?
- [ ] 是否有直接的跨进程调用?
## 💡 最佳实践
### 1. 渐进式开发
1. 先在Web环境实现和测试
2. 定义完整的接口
3. 实现Electron代理
4. 完善IPC链路
5. 在Desktop环境测试
### 2. 调试技巧
```javascript
// 在每个环节添加日志
console.log('[Vue] Calling method:', methodName);
console.log('[Proxy] Forwarding to IPC:', methodName);
console.log('[Main] Handling IPC:', methodName);
console.log('[Service] Executing:', methodName);
```
### 3. 类型安全
```typescript
// 使用严格的类型检查
interface ElectronAPI {
template: {
[K in keyof ITemplateManager]: ITemplateManager[K];
};
}
```
## 🔗 相关经验
这些架构分析为后续的开发提供了基础:
- 建立了完整的IPC开发流程
- 形成了接口优先的设计原则
- 建立了完整的开发和调试检查清单
这些经验在后续的序列化优化(115)中得到了进一步应用。
@@ -1,4 +1,4 @@
# 功能提示词管理语言切换按钮修复
# 语言切换按钮修复
## 🎯 问题描述
@@ -28,130 +28,66 @@ export interface ITemplateLanguageService {
toggleLanguage(): Promise<BuiltinTemplateLanguage>;
isValidLanguage(language: string): Promise<boolean>;
getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]>;
getLanguageDisplayName(language: BuiltinTemplateLanguage): string;
isInitialized(): boolean;
}
```
### 2. 修复UI组件异步调用
修复`BuiltinTemplateLanguageSwitch.vue`组件:
### 2. 修复Vue组件异步调用
```vue
<!-- 修复前 -->
<template>
<span>{{ currentLanguage }}</span> <!-- 显示 "Object Promise" -->
</template>
<script>
const currentLanguage = computed(() => {
return service.getCurrentLanguage(); // 返回 Promise
});
</script>
<span>{{ languageService.getCurrentLanguage() }}</span>
<!-- 修复后 -->
<template>
<span>{{ currentLanguage }}</span> <!-- 显示 "中文" -->
</template>
<span>{{ currentLanguage }}</span>
<script>
const currentLanguage = ref('');
<script setup>
const currentLanguage = ref('')
const loadCurrentLanguage = async () => {
currentLanguage.value = await service.getCurrentLanguage(); // 正确使用 await
};
onMounted(() => {
loadCurrentLanguage();
});
onMounted(async () => {
currentLanguage.value = await languageService.getCurrentLanguage()
})
</script>
```
### 3. 修复Electron代理类
修正`ElectronTemplateLanguageServiceProxy`的方法签名:
```typescript
// 修复前 - 错误的同步方法
getCurrentLanguage(): BuiltinTemplateLanguage {
return this.electronAPI.getCurrentBuiltinTemplateLanguage(); // 返回 Promise
}
// 修复后 - 正确的异步方法
async getCurrentLanguage(): Promise<BuiltinTemplateLanguage> {
return await this.electronAPI.getCurrentBuiltinTemplateLanguage();
}
```
### 4. 完善IPC通信链路
在preload.js和main.js中添加完整的IPC处理:
### 3. 完善IPC调用链
```javascript
// preload.js
getCurrentBuiltinTemplateLanguage: async () => {
const result = await ipcRenderer.invoke('template-getCurrentLanguage');
if (!result.success) throw new Error(result.error);
return result.data;
},
templateLanguage: {
getCurrentLanguage: async () => {
const result = await ipcRenderer.invoke('template-getCurrentBuiltinTemplateLanguage');
if (!result.success) throw new Error(result.error);
return result.data;
}
}
// main.js
ipcMain.handle('template-getCurrentLanguage', async () => {
ipcMain.handle('template-getCurrentBuiltinTemplateLanguage', async (event) => {
try {
const result = await templateLanguageService.getCurrentLanguage();
return { success: true, data: result };
const result = await templateManager.getCurrentBuiltinTemplateLanguage();
return createSuccessResponse(result);
} catch (error) {
return { success: false, error: error.message };
return createErrorResponse(error);
}
});
```
## ✅ 修复结果
## ✅ 修复验证
### 功能验证
### 验证清单
- [x] 语言切换按钮正确显示"中文"或"English"
- [x] 语言切换功能正常工作
- [x] 完全解决了"Object Promise"显示问题
- [x] Web和Electron环境行为一致
- [x] 所有相关功能测试通过
- [x] 所有异步调用正确处理
### 技术改进
- [x] 统一了异步接口设计
- [x] 修复了所有IPC调用的异步处理
- [x] 应用了立即错误原则
- [x] 提高了类型安全性
## 💡 经验总结
## 🛠️ 技术要点
### 核心原则
1. **接口一致性**: 跨环境的接口必须保持一致的异步性
2. **错误处理**: 让错误自然传播,便于问题定位
3. **类型安全**: 使用TypeScript确保接口实现的完整性
### Promise对象显示问题诊断
当UI显示"Object Promise"时,通常表示:
1. 异步方法被当作同步使用
2. 缺少await关键字
3. 接口定义与实现不匹配
### 最佳实践
1. **统一异步**: 所有跨环境接口都应该是异步的
2. **接口驱动**: 先定义接口,再实现具体类
3. **完整测试**: 在两种环境下都要验证功能
### 诊断步骤
1. 检查显示异常的UI组件
2. 追踪数据来源的方法调用
3. 确认方法是否返回Promise对象
4. 检查调用方是否使用await
### 修复模式
- **识别异步方法**: 检查方法是否返回Promise
- **正确使用await**: 在调用异步方法时使用await
- **统一接口设计**: 确保所有环境使用相同的异步接口
- **完善错误处理**: 让错误自然传播,便于调试
## 🚫 避坑指南
### 异步接口设计陷阱
- **陷阱**: 在不同环境中使用不同的方法签名(同步 vs 异步)
- **避免**: 设计接口时优先考虑最复杂环境的需求(通常是Electron)
- **原则**: 如果任何环境需要异步,所有环境都使用异步接口
### UI组件异步处理陷阱
- **陷阱**: 在computed中直接调用异步方法
- **避免**: 使用ref存储异步结果,在生命周期钩子中加载数据
- **原则**: 异步数据加载应该在组件初始化时完成
### IPC调用处理陷阱
- **陷阱**: 忘记在IPC调用中使用await
- **避免**: 严格遵循异步调用规范
- **原则**: 所有IPC调用都是异步的,必须正确处理
这次修复建立了完整的跨环境异步接口调用规范,为后续类似问题的解决提供了标准模式。
这个修复建立了完整的异步接口设计模式,为后续的IPC开发提供了标准。
@@ -81,3 +81,117 @@ const storage = new FileStorageProvider(userDataPath) // 新方式
- **集成测试**: 正确模拟UI层历史记录保存行为
修复后测试结果:291个测试通过,9个跳过 ✅
## 🔧 后续修复补充
### 应用退出无限循环问题修复
**问题发现**: 在使用FileStorageProvider后,发现应用退出时出现无限循环保存数据的问题。
**问题表现**:
```
[DESKTOP] Saving data before quit...
[DESKTOP] Data saved successfully
[DESKTOP] Saving data before quit...
[DESKTOP] Data saved successfully
```
**根本原因**:
1. 数据保存失败时`isDirty`标志未重置
2. 退出事件处理器形成循环:`window.close` → `before-quit` → `app.quit()` → `before-quit`
**解决方案**:
#### 1. FileStorageProvider防护机制
```javascript
async flush(): Promise<void> {
// 检查重试次数限制
if (this.flushAttempts >= this.MAX_FLUSH_ATTEMPTS) {
console.error('Max flush attempts reached, forcing isDirty to false');
this.isDirty = false;
this.flushAttempts = 0;
throw new Error('Max flush attempts exceeded');
}
try {
await Promise.race([
this.saveToFile(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Flush timeout')), this.MAX_FLUSH_TIME)
)
]);
this.isDirty = false;
this.flushAttempts = 0;
} catch (error) {
// 强制重置状态避免无限重试
if (this.flushAttempts >= this.MAX_FLUSH_ATTEMPTS) {
this.isDirty = false;
this.flushAttempts = 0;
}
throw error;
}
}
```
#### 2. 多层应用退出保护机制
```javascript
let isQuitting = false;
const MAX_SAVE_TIME = 5000;
// 应急退出:10秒后强制终止
function setupEmergencyExit() {
const emergencyExitTimer = setTimeout(() => {
console.error('[DESKTOP] EMERGENCY EXIT: Force terminating process');
process.exit(1);
}, 10000);
return emergencyExitTimer;
}
app.on('before-quit', async (event) => {
if (!isQuitting && storageProvider) {
event.preventDefault();
isQuitting = true;
const emergencyTimer = setupEmergencyExit();
try {
await Promise.race([
storageProvider.flush(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Save timeout')), MAX_SAVE_TIME - 1000)
)
]);
} catch (error) {
console.error('Save failed:', error);
} finally {
clearTimeout(emergencyTimer);
setImmediate(() => {
isQuitting = false;
app.quit();
});
}
}
});
```
#### 3. 防护机制层级
- **逻辑保护**: `isQuitting`标志防止重复执行
- **超时保护**: 5秒强制关闭窗口/退出应用
- **应急保护**: 10秒强制终止进程
- **系统保护**: 响应SIGINT/SIGTERM信号
### 经验总结
#### 文件存储退出处理原则
1. **多层保护**: 实现多个层级的保护机制
2. **超时控制**: 避免无限等待数据保存
3. **状态重置**: 异常情况下强制重置状态
4. **优雅降级**: 保存失败也要确保应用能退出
#### 最佳实践
- 在FileStorageProvider中实现重试限制和超时保护
- 在应用层实现多层退出保护机制
- 使用Promise.race实现超时控制
- 建立完整的异常处理和状态重置机制
这些补充修复确保了FileStorageProvider在各种异常情况下都能正常工作,并且应用能够可靠地退出。
@@ -0,0 +1,314 @@
# 115-IPC序列化修复与数据一致性
## 📋 概述
解决Electron应用中Vue响应式对象IPC序列化问题,以及由此引发的数据一致性问题。
**📝 专注领域**:本文档专注于Vue响应式对象的IPC序列化问题,其他IPC架构问题请参考[112-desktop-ipc-fixes](../112-desktop-ipc-fixes/)。
## 🚨 核心问题
### 1. IPC序列化错误
```
An object could not be cloned
```
**原因**:Vue响应式对象包含不可序列化的属性(Proxy、Symbol等),无法通过Electron IPC传递。
### 2. 数据一致性问题
```
修改gemini模型apiKey → 其他模型(openai, deepseek等)全部消失
```
**根本原因**:ModelManager的updateData回调函数基于不完整的存储数据进行操作。
## ✅ 解决方案
### 1. IPC层序列化保护
#### safeSerialize函数
```typescript
/**
* 安全序列化函数,用于清理Vue响应式对象
* 确保所有通过IPC传递的对象都是纯净的JavaScript对象
*/
function safeSerialize(obj) {
if (obj === null || obj === undefined) {
return obj;
}
// 对于基本类型,直接返回
if (typeof obj !== 'object') {
return obj;
}
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error('[IPC Serialization] Failed to serialize object:', error);
throw new Error(`Failed to serialize object for IPC: ${error.message}`);
}
}
```
#### IPC处理器应用
```typescript
// 模型管理相关
ipcMain.handle('model-updateModel', async (event, id, updates) => {
try {
const safeUpdates = safeSerialize(updates);
await modelManager.updateModel(id, safeUpdates);
return createSuccessResponse(null);
} catch (error) {
return createErrorResponse(error);
}
});
ipcMain.handle('model-addModel', async (event, model) => {
try {
const safeModel = safeSerialize(model);
const { key, ...config } = safeModel;
await modelManager.addModel(key, config);
return createSuccessResponse(null);
} catch (error) {
return createErrorResponse(error);
}
});
// 模板管理相关
ipcMain.handle('template-createTemplate', async (event, template) => {
try {
const safeTemplate = safeSerialize(template);
await templateManager.saveTemplate(safeTemplate);
return createSuccessResponse(null);
} catch (error) {
return createErrorResponse(error);
}
});
ipcMain.handle('template-updateTemplate', async (event, id, updates) => {
try {
const existingTemplate = await templateManager.getTemplate(id);
const safeUpdates = safeSerialize(updates);
const updatedTemplate = { ...existingTemplate, ...safeUpdates, id };
await templateManager.saveTemplate(updatedTemplate);
return createSuccessResponse(null);
} catch (error) {
return createErrorResponse(error);
}
});
// 历史记录相关
ipcMain.handle('history-addRecord', async (event, record) => {
try {
const safeRecord = safeSerialize(record);
const result = await historyManager.addRecord(safeRecord);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(error);
}
});
ipcMain.handle('history-createNewChain', async (event, record) => {
try {
const safeRecord = safeSerialize(record);
const result = await historyManager.createNewChain(safeRecord);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(error);
}
});
ipcMain.handle('history-addIteration', async (event, params) => {
try {
const safeParams = safeSerialize(params);
const result = await historyManager.addIteration(safeParams);
return createSuccessResponse(result);
} catch (error) {
return createErrorResponse(error);
}
});
```
### 2. 业务逻辑层数据一致性修复
#### 问题根因
ModelManager的updateData回调函数错误地基于可能不完整的存储数据:
```typescript
// ❌ 错误的实现
(currentModels) => {
const models = currentModels || {}; // 可能不完整!
return {
...models, // 基于不完整的数据
[key]: updatedConfig
};
}
```
#### 正确的解决方案
```typescript
// ✅ 正确的实现
(currentModels) => {
// 使用内存中的完整模型列表作为基础
const models = { ...this.models };
// 如果存储中有数据,合并到内存状态中
if (currentModels) {
Object.assign(models, currentModels);
}
return {
...models, // 完整的模型列表
[key]: updatedConfig
};
}
```
#### 修复范围
所有ModelManager的数据更新方法:
1. **addModel** - 添加模型时保持完整列表
2. **updateModel** - 更新模型时保持完整列表
3. **deleteModel** - 删除模型时基于完整列表
4. **enableModel** - 启用模型时保持完整列表
5. **disableModel** - 禁用模型时保持完整列表
### 3. 双重保护机制
```
Vue组件 → safeSerialize → IPC → 业务逻辑修复 → 增强的FileStorageProvider
↑ ↑ ↑
清理响应式对象 数据完整性保障 原子性操作+备份保护
```
## 🛡️ 核心原则
### 1. 分层修复原则
**在正确的层级解决对应的问题**
- **IPC传输问题** → IPC层 (main.js)
- **业务逻辑错误** → 业务逻辑层 (ModelManager)
- **存储安全问题** → 存储层 (FileStorageProvider)
### 2. 数据完整性优先原则
**始终基于完整的数据进行操作**
```typescript
// 错误:基于可能不完整的存储状态
const models = currentModels || {};
// 正确:基于内存中的完整状态
const models = { ...this.models };
if (currentModels) {
Object.assign(models, currentModels);
}
```
### 3. 边界清理原则
**在IPC边界清理Vue响应式对象**
```typescript
// 在IPC处理器中统一清理
const safeData = safeSerialize(reactiveData);
```
## 🧪 测试验证
### 1. IPC序列化测试
```typescript
describe('IPC Serialization', () => {
it('should handle Vue reactive objects', async () => {
const reactiveObj = reactive({ key: 'value', nested: { prop: 'test' } });
const serialized = safeSerialize(reactiveObj);
expect(serialized).toEqual({ key: 'value', nested: { prop: 'test' } });
expect(typeof serialized).toBe('object');
expect(serialized.constructor).toBe(Object);
});
});
```
### 2. 数据一致性测试
```typescript
describe('Data Consistency', () => {
it('should maintain complete model list when updating single model', async () => {
// 初始化完整的模型列表
const initialModels = { openai: config1, gemini: config2, deepseek: config3 };
// 更新单个模型
await modelManager.updateModel('gemini', { apiKey: 'new-key' });
// 验证其他模型没有丢失
const allModels = await modelManager.getAllModels();
expect(Object.keys(allModels)).toHaveLength(3);
expect(allModels.openai).toBeDefined();
expect(allModels.deepseek).toBeDefined();
});
});
```
## 📊 技术价值
### 1. 问题解决
- ✅ 彻底解决IPC序列化错误
- ✅ 修复数据丢失问题
- ✅ 建立数据一致性保障机制
### 2. 架构完善
- ✅ 分层修复,职责清晰
- ✅ 双重保护机制
- ✅ 统一的错误处理
### 3. 开发体验
- ✅ 透明的序列化处理
- ✅ 可靠的数据操作
- ✅ 完善的测试覆盖
## 🔗 相关文档
- [114-desktop-file-storage](../114-desktop-file-storage/) - 存储层安全增强
- [112-desktop-ipc-fixes](../112-desktop-ipc-fixes/) - 早期IPC修复经验
## 💡 最佳实践
### IPC序列化
- ✅ 在ElectronProxy层统一处理序列化(已完成)
- ✅ 使用通用的safeSerializeForIPC函数(已完成)
- ✅ 保持调用方的透明性(已完成)
- ✅ 清理UI层的手动序列化代码(已完成)
### 数据一致性
- 基于完整的内存状态进行更新
- 合并存储中的增量更新
- 确保返回完整的数据集
### 错误处理
- 在正确的层级处理对应的错误
- 提供详细的错误信息
- 建立完整的错误恢复机制
### 架构演进
这些修复经历了两个阶段:
1. **第一阶段**:在UI层手动序列化(112-desktop-ipc-fixes)
2. **第二阶段**:移到ElectronProxy层自动序列化(当前方案)
最终实现了对Vue组件完全透明的IPC序列化处理,确保了Electron应用中数据操作的可靠性和一致性。
## 📁 文档结构
本目录包含以下文档:
- **README.md** - 主要概述和最佳实践
- **proxy-layer-serialization.md** - ElectronProxy层序列化技术实现
- **architecture-evolution.md** - 架构演进完整记录
## 🔗 相关文档
- [112-Desktop IPC修复](../112-desktop-ipc-fixes/) - IPC架构问题和语言切换修复
- [Electron IPC最佳实践](../../developer/electron-ipc-best-practices.md) - 当前开发指南
## 💡 文档分工
**112专注于**:IPC架构完整性、异步接口设计、语言切换等功能性问题
**115专注于**:Vue响应式对象序列化、ElectronProxy层自动化处理
@@ -0,0 +1,148 @@
# 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. **完整测试** - 确保架构变更的可靠性
### 避免的陷阱
- ❌ 过度工程化(如装饰器方案)
- ❌ 在错误的层级解决问题
- ❌ 忽视开发体验
这次架构演进是一个很好的例子,展示了如何通过合理的架构设计来解决技术问题,同时提升开发体验。
@@ -0,0 +1,220 @@
# ElectronProxy层统一IPC序列化处理
## 📋 概述
将IPC序列化处理从UI层移动到ElectronProxy层,实现统一的、对Vue组件透明的序列化处理机制。
## 🚨 问题背景
### 原有方案的问题
1. **需要在每个Vue组件中手动序列化** - 容易遗漏,维护成本高
2. **开发者心智负担重** - 需要记住在每个IPC调用前序列化
3. **架构不合理** - UI层需要关心底层IPC实现细节
4. **容易出错** - 新增功能时容易忘记序列化处理
### 错误发生的真实原因
虽然main.js中有`safeSerialize`处理,但错误发生在**IPC传输阶段**:
```
Vue组件 → ElectronProxy → preload.js → [IPC传输] → main.js
↑
错误发生在这里
```
## ✅ 解决方案
### 1. 统一序列化工具
**文件**: `packages/core/src/utils/ipc-serialization.ts`
```typescript
/**
* 安全序列化函数,用于清理Vue响应式对象
*/
export function safeSerializeForIPC<T>(obj: T): T {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj !== 'object') {
return obj;
}
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error('[IPC Serialization] Failed to serialize object:', error);
throw new Error(`Failed to serialize object for IPC: ${error instanceof Error ? error.message : String(error)}`);
}
}
```
### 2. ElectronProxy层自动序列化
#### TemplateManager代理
```typescript
// packages/core/src/services/template/electron-proxy.ts
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
export class ElectronTemplateManagerProxy implements ITemplateManager {
async saveTemplate(template: Template): Promise<void> {
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeTemplate = safeSerializeForIPC(template);
return this.electronAPI.createTemplate(safeTemplate);
}
}
```
#### ModelManager代理
```typescript
// packages/core/src/services/model/electron-proxy.ts
export class ElectronModelManagerProxy implements IModelManager {
async addModel(key: string, config: ModelConfig): Promise<void> {
const safeConfig = safeSerializeForIPC({ ...config, key });
await this.electronAPI.model.addModel(safeConfig);
}
async updateModel(key: string, config: Partial<ModelConfig>): Promise<void> {
const safeConfig = safeSerializeForIPC(config);
await this.electronAPI.model.updateModel(key, safeConfig);
}
}
```
#### HistoryManager代理
```typescript
// packages/core/src/services/history/electron-proxy.ts
export class ElectronHistoryManagerProxy implements IHistoryManager {
async addRecord(record: PromptRecord): Promise<void> {
const safeRecord = safeSerializeForIPC(record);
return this.electronAPI.history.addRecord(safeRecord);
}
async createNewChain(record: Omit<PromptRecord, 'chainId' | 'version' | 'previousId'>): Promise<PromptRecordChain> {
const safeRecord = safeSerializeForIPC(record);
return this.electronAPI.history.createNewChain(safeRecord);
}
async addIteration(params: {...}): Promise<PromptRecordChain> {
const safeParams = safeSerializeForIPC(params);
return this.electronAPI.history.addIteration(safeParams);
}
}
```
#### PromptService代理
```typescript
// packages/core/src/services/prompt/electron-proxy.ts
export class ElectronPromptServiceProxy implements IPromptService {
async optimizePrompt(request: OptimizationRequest): Promise<string> {
const safeRequest = safeSerializeForIPC(request);
return this.api.optimizePrompt(safeRequest);
}
}
```
### 3. Vue组件简化
现在Vue组件可以直接调用服务,无需关心序列化:
```typescript
// TemplateManager.vue - 修复前
import { createSafeTemplate } from '../utils/ipc-serialization'
const safeTemplate = createSafeTemplate(updatedTemplate)
await getTemplateManager.value.saveTemplate(safeTemplate)
// TemplateManager.vue - 修复后
await getTemplateManager.value.saveTemplate(updatedTemplate) // 自动序列化
```
## 🏗️ 架构优势
### 1. 分层清晰
```
Vue组件层 - 业务逻辑,无需关心IPC细节
↓
ElectronProxy层 - 自动序列化,IPC调用
↓
IPC传输层 - 纯净JavaScript对象传输
↓
Main进程层 - 双重保护(safeSerialize)
```
### 2. 开发体验
- ✅ **对Vue组件透明** - 组件无需关心序列化
- ✅ **自动保护** - 新增功能自动获得序列化保护
- ✅ **集中管理** - 所有序列化逻辑在一个地方
- ✅ **不易遗漏** - 架构层面保证序列化处理
### 3. 维护性
- ✅ **统一工具** - 避免重复代码
- ✅ **类型安全** - TypeScript类型检查
- ✅ **错误处理** - 统一的错误处理机制
## 🛡️ 双重保护机制
```
Vue组件 → ElectronProxy序列化 → IPC传输 → Main.js序列化 → 业务逻辑
↑ ↑
第一层保护 第二层保护
(必需,解决传输问题) (防御性,处理边缘情况)
```
## 📊 修复验证
### 修复的文件
- ✅ `packages/core/src/utils/ipc-serialization.ts` - 统一序列化工具
- ✅ `packages/core/src/services/template/electron-proxy.ts` - 模板管理代理
- ✅ `packages/core/src/services/model/electron-proxy.ts` - 模型管理代理
- ✅ `packages/core/src/services/history/electron-proxy.ts` - 历史记录代理
- ✅ `packages/core/src/services/prompt/electron-proxy.ts` - 提示词服务代理
- ✅ `packages/core/src/services/llm/electron-proxy.ts` - LLM服务代理
- ✅ `packages/core/src/services/preference/electron-proxy.ts` - 偏好设置代理
- ✅ `packages/core/src/index.ts` - 导出序列化工具
### 清理的文件
- ✅ `packages/ui/src/utils/ipc-serialization.ts` - 删除UI层序列化工具
- ✅ `packages/ui/src/components/TemplateManager.vue` - 移除手动序列化
- ✅ `packages/ui/src/components/ModelManager.vue` - 移除手动序列化
- ✅ `packages/ui/src/composables/usePromptOptimizer.ts` - 移除手动序列化
- ✅ `packages/ui/src/composables/usePromptHistory.ts` - 移除手动序列化
### 测试场景
- [ ] 模板迁移功能(原问题场景)
- [ ] 模型添加/编辑功能
- [ ] 历史记录保存功能
- [ ] 提示词优化功能
## 💡 最佳实践
### 1. 新增ElectronProxy方法时
```typescript
async newMethod(complexObject: SomeType): Promise<ResultType> {
// 总是序列化复杂对象参数
const safeObject = safeSerializeForIPC(complexObject);
return this.electronAPI.someService.newMethod(safeObject);
}
```
### 2. 基本类型参数无需序列化
```typescript
async simpleMethod(id: string, count: number): Promise<void> {
// 基本类型无需序列化
return this.electronAPI.someService.simpleMethod(id, count);
}
```
### 3. 调试序列化问题
```typescript
import { debugIPCSerializability } from '@prompt-optimizer/core';
// 开发时检查对象是否可序列化
debugIPCSerializability(complexObject, 'MyObject');
```
## 🎯 总结
这次修复实现了:
1. **架构优化** - 将序列化处理移到正确的层级
2. **开发体验提升** - Vue组件无需关心IPC细节
3. **维护性改善** - 统一的序列化处理,避免重复代码
4. **可靠性增强** - 双重保护机制,确保IPC传输安全
通过这种方式,我们彻底解决了"An object could not be cloned"错误,同时建立了可持续的架构模式。
+9 -6
View File
@@ -72,6 +72,7 @@
### 进程间通信
- **[112-desktop-ipc-fixes](./112-desktop-ipc-fixes/)** - 桌面端IPC修复合集
- 语言切换按钮显示"Object Promise"问题修复
- Vue响应式对象IPC序列化问题修复
- IPC架构分析与修复
- 跨环境异步接口统一
- preload.js架构规范化
@@ -94,15 +95,17 @@
### 按问题类型查找
- **启动问题** → 102-web-architecture-refactor
- **显示异常** → 112-desktop-ipc-fixes
- **存储问题** → 110-desktop-indexeddb-fix
- **存储问题** → 110-desktop-indexeddb-fix, 114-desktop-file-storage
- **序列化错误** → 112-desktop-ipc-fixes
- **应用退出问题** → 114-desktop-file-storage
- **语言设置问题** → 112-desktop-ipc-fixes
- **布局问题** → 108-layout-system
- **主题问题** → 109-theme-system
- **模板问题** → 106-template-management
- **组件问题** → 107-component-standardization
- **存储问题** → 114-desktop-file-storage
### 按技术栈查找
- **Electron相关** → 103, 110, 111, 112
- **Electron相关** → 103, 110, 111, 112, 114
- **Vue/前端相关** → 102, 104, 105, 107, 108, 109
- **架构设计相关** → 101, 102, 103, 111, 113
- **服务层相关** → 101, 106, 113
@@ -112,14 +115,14 @@
- **项目初期架构** → 101, 102, 103
- **功能开发阶段** → 104, 105, 106, 107
- **优化改进阶段** → 108, 109
- **问题修复阶段** → 110, 111, 112
- **问题修复阶段** → 110, 111, 112, 114
- **重构完善阶段** → 113
### 按经验类型查找
- **架构设计经验** → 101, 102, 103, 111
- **功能开发经验** → 106, 107
- **UI/UX设计经验** → 108, 109
- **问题排查经验** → 110, 112
- **问题排查经验** → 110, 112, 114
- **重构实践经验** → 101, 113
## 📖 使用建议
@@ -128,7 +131,7 @@
1. **了解架构** → 101 → 102 → 103
2. **学习功能开发** → 106 → 107
3. **掌握系统优化** → 108 → 109
4. **学习问题排查** → 110 → 112
4. **学习问题排查** → 110 → 112 → 114
### 问题解决路径
1. **确定问题类型** → 查看"按问题类型查找"
@@ -0,0 +1,163 @@
# Electron IPC 最佳实践
## 问题背景
在Electron应用中,Vue的响应式对象不能直接通过IPC(进程间通信)传递,会导致"An object could not be cloned"错误。这是因为Vue的响应式对象包含了不可序列化的代理包装器。
## 核心原则
### 1. ElectronProxy层自动处理序列化
✅ **现在的做法**:
```javascript
// 可以直接传递Vue响应式对象,ElectronProxy会自动序列化
await modelManager.addModel(newModel.value.key, {
name: newModel.value.name,
llmParams: newModel.value.llmParams // ElectronProxy会自动清理响应式包装
})
```
**架构优势**:
- Vue组件无需关心序列化细节
- 所有序列化逻辑集中在ElectronProxy层
- 自动保护,不易遗漏
- 代码更简洁,开发体验更好
### 2. 自动序列化处理
**ElectronProxy层自动处理序列化**:
- 所有ElectronProxy类已经内置了序列化处理
- Vue组件无需手动调用序列化函数
- 直接传递Vue响应式对象即可,代理层会自动清理
**技术实现**:
- 使用 `packages/core/src/utils/ipc-serialization.ts` 中的 `safeSerializeForIPC` 函数
- 在每个需要的ElectronProxy方法中自动调用序列化
- 确保100%的IPC兼容性
### 3. 识别问题的方法
当你看到以下错误时,说明存在IPC序列化问题:
- `An object could not be cloned`
- `DataCloneError`
- `Failed to execute 'postMessage'`
## 常见问题场景
### 1. 模型管理
```javascript
// ✅ 现在可以直接传递Vue响应式对象
await modelManager.addModel(key, {
llmParams: formData.value.llmParams // ElectronProxy会自动序列化
})
```
### 2. 历史记录
```javascript
// ✅ 现在可以直接传递Vue响应式对象
await historyManager.createNewChain({
metadata: { mode: optimizationMode.value } // ElectronProxy会自动序列化
})
```
### 3. 模板管理
```javascript
// ✅ 现在可以直接传递Vue响应式对象
await templateManager.saveTemplate({
content: form.value.messages // ElectronProxy会自动序列化
})
```
## 开发检查清单
现在开发更简单了,只需要检查:
- [ ] 在desktop环境下是否测试过?
- [ ] 是否有直接的IPC调用绕过了ElectronProxy?
- [ ] 新增的ElectronProxy方法是否包含了序列化处理?
## 调试技巧
### 1. 检查对象类型
```javascript
console.log('Object type:', Object.prototype.toString.call(obj))
console.log('Is reactive:', obj.__v_isReactive)
console.log('Is ref:', obj.__v_isRef)
```
### 2. 测试序列化
```javascript
try {
JSON.stringify(obj)
console.log('Object is serializable')
} catch (error) {
console.error('Object is not serializable:', error)
}
```
### 3. 使用开发工具
在Chrome DevTools中,响应式对象会显示为 `Proxy` 类型。
## 架构建议
### 1. ElectronProxy层统一处理
序列化处理已经移到ElectronProxy层,Vue组件可以直接调用:
```javascript
// 在组件方法中 - 现在更简单了
const handleSave = async () => {
await service.save(formData.value) // 直接传递,无需手动序列化
}
```
### 2. 新增ElectronProxy方法的规范
当添加新的ElectronProxy方法时,对复杂对象参数进行序列化:
```typescript
async newMethod(complexObject: SomeType): Promise<ResultType> {
// 对复杂对象参数进行序列化
const safeObject = safeSerializeForIPC(complexObject);
return this.electronAPI.someService.newMethod(safeObject);
}
```
### 3. 类型安全
ElectronProxy的接口应该接受Vue响应式对象,内部自动处理:
```typescript
interface IModelManager {
addModel(key: string, config: ModelConfig | Ref<ModelConfig>): Promise<void>
// 接口层面支持响应式对象,实现层面自动序列化
}
```
## 性能考虑
- ElectronProxy层使用 `JSON.parse(JSON.stringify())` 确保100%兼容性
- 序列化只在IPC边界发生,不影响Vue组件性能
- 避免在渲染循环中进行频繁的服务调用
- 对于大型对象,考虑分批处理或使用更细粒度的数据传递
## 测试策略
1. **单元测试**:确保序列化函数正确处理各种数据类型
2. **集成测试**:在desktop环境下测试所有IPC调用
3. **回归测试**:每次修改涉及IPC的代码后,都要在desktop环境下测试
## 总结
现在的架构已经大大简化了Electron IPC的使用:
1. **Vue组件层**:直接传递响应式对象,无需关心序列化
2. **ElectronProxy层**:自动处理序列化,确保IPC兼容性
3. **Main进程层**:双重保护,处理边缘情况
4. **开发体验**:更简洁的代码,更少的出错机会
记住:**现在可以放心地传递Vue响应式对象,架构会自动处理!**
## 📚 相关文档
- [112-Desktop IPC修复](../archives/112-desktop-ipc-fixes/) - IPC架构分析和语言切换修复
- [115-IPC序列化修复](../archives/115-ipc-serialization-fixes/) - Vue响应式对象序列化解决方案
- [ElectronProxy层序列化](../archives/115-ipc-serialization-fixes/proxy-layer-serialization.md) - 技术实现细节
- [架构演进记录](../archives/115-ipc-serialization-fixes/architecture-evolution.md) - 从手动到自动的演进过程
+4 -1
View File
@@ -63,4 +63,7 @@ export { ElectronPreferenceServiceProxy } from './services/preference/electron-p
export { PreferenceService, createPreferenceService } from './services/preference/service'
// 导出环境检测工具
export { isRunningInElectron, isElectronApiReady, waitForElectronApi, checkVercelApiAvailability, resetVercelStatusCache, isBrowser, getProxyUrl } from './utils/environment'
export { isRunningInElectron, isElectronApiReady, waitForElectronApi, checkVercelApiAvailability, resetVercelStatusCache, isBrowser, getProxyUrl } from './utils/environment'
// 导出IPC序列化工具
export { safeSerializeForIPC, debugIPCSerializability, safeSerializeArgs } from './utils/ipc-serialization'
@@ -1,4 +1,5 @@
import type { IHistoryManager, PromptRecord, PromptRecordChain } from './types';
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
/**
* Electron环境下的历史记录管理器代理
@@ -13,7 +14,9 @@ export class ElectronHistoryManagerProxy implements IHistoryManager {
}
async addRecord(record: PromptRecord): Promise<void> {
return this.electronAPI.history.addRecord(record);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeRecord = safeSerializeForIPC(record);
return this.electronAPI.history.addRecord(safeRecord);
}
async getRecords(): Promise<PromptRecord[]> {
@@ -50,7 +53,9 @@ export class ElectronHistoryManagerProxy implements IHistoryManager {
}
async createNewChain(record: Omit<PromptRecord, 'chainId' | 'version' | 'previousId'>): Promise<PromptRecordChain> {
return this.electronAPI.history.createNewChain(record);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeRecord = safeSerializeForIPC(record);
return this.electronAPI.history.createNewChain(safeRecord);
}
async addIteration(params: {
@@ -61,7 +66,9 @@ export class ElectronHistoryManagerProxy implements IHistoryManager {
modelKey: string;
templateId: string;
}): Promise<PromptRecordChain> {
return this.electronAPI.history.addIteration(params);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeParams = safeSerializeForIPC(params);
return this.electronAPI.history.addIteration(safeParams);
}
async deleteChain(chainId: string): Promise<void> {
@@ -1,4 +1,5 @@
import { ILLMService, Message, StreamHandlers, LLMResponse, ModelOption } from './types';
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
/**
* Electron环境下的LLM服务代理
@@ -20,11 +21,15 @@ export class ElectronLLMProxy implements ILLMService {
}
async sendMessage(messages: Message[], provider: string): Promise<string> {
return this.electronAPI.llm.sendMessage(messages, provider);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeMessages = safeSerializeForIPC(messages);
return this.electronAPI.llm.sendMessage(safeMessages, provider);
}
async sendMessageStructured(messages: Message[], provider: string): Promise<LLMResponse> {
return this.electronAPI.llm.sendMessageStructured(messages, provider);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeMessages = safeSerializeForIPC(messages);
return this.electronAPI.llm.sendMessageStructured(safeMessages, provider);
}
async sendMessageStream(
@@ -32,6 +37,9 @@ export class ElectronLLMProxy implements ILLMService {
provider: string,
callbacks: StreamHandlers
): Promise<void> {
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeMessages = safeSerializeForIPC(messages);
// 适配回调接口:StreamHandlers 使用 onToken,而 preload 期望的是 onContent
const adaptedCallbacks = {
onContent: callbacks.onToken, // 映射 onToken -> onContent
@@ -39,15 +47,17 @@ export class ElectronLLMProxy implements ILLMService {
onFinish: () => callbacks.onComplete(), // 映射完成回调
onError: callbacks.onError
};
await this.electronAPI.llm.sendMessageStream(messages, provider, adaptedCallbacks);
await this.electronAPI.llm.sendMessageStream(safeMessages, provider, adaptedCallbacks);
}
async fetchModelList(
provider: string,
customConfig?: Partial<any>
): Promise<ModelOption[]> {
const modelNames = await this.electronAPI.llm.fetchModelList(provider, customConfig);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeCustomConfig = customConfig ? safeSerializeForIPC(customConfig) : customConfig;
const modelNames = await this.electronAPI.llm.fetchModelList(provider, safeCustomConfig);
// Convert string array to ModelOption array
return modelNames.map(name => ({ value: name, label: name }));
}
@@ -1,4 +1,5 @@
import { IModelManager, ModelConfig } from './types';
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
/**
* Electron环境下的ModelManager代理
@@ -37,11 +38,15 @@ export class ElectronModelManagerProxy implements IModelManager {
}
async addModel(key: string, config: ModelConfig): Promise<void> {
await this.electronAPI.model.addModel({ ...config, key });
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeConfig = safeSerializeForIPC({ ...config, key });
await this.electronAPI.model.addModel(safeConfig);
}
async updateModel(key: string, config: Partial<ModelConfig>): Promise<void> {
await this.electronAPI.model.updateModel(key, config);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeConfig = safeSerializeForIPC(config);
await this.electronAPI.model.updateModel(key, safeConfig);
}
async deleteModel(key: string): Promise<void> {
@@ -1,4 +1,5 @@
import type { IPreferenceService } from './types';
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
declare const window: {
electronAPI: {
@@ -21,7 +22,9 @@ export class ElectronPreferenceServiceProxy implements IPreferenceService {
async set<T>(key: string, value: T): Promise<void> {
this.ensureApiAvailable();
return window.electronAPI.preference.set(key, value);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeValue = safeSerializeForIPC(value);
return window.electronAPI.preference.set(key, safeValue);
}
async delete(key: string): Promise<void> {
@@ -4,6 +4,7 @@ import {
StreamHandlers,
} from './types';
import { PromptRecord } from '../history/types';
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
// Helper function to check if running in Electron renderer process
function isRunningInElectron(): boolean {
@@ -32,7 +33,9 @@ export class ElectronPromptServiceProxy implements IPromptService {
}
async optimizePrompt(request: OptimizationRequest): Promise<string> {
return this.api.optimizePrompt(request);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeRequest = safeSerializeForIPC(request);
return this.api.optimizePrompt(safeRequest);
}
async iteratePrompt(
@@ -24,6 +24,9 @@ export class FileStorageProvider implements IStorageProvider {
// 配置常量
private readonly WRITE_DELAY = 500; // 500ms延迟写入
private readonly TEMP_FILE_SUFFIX = '.tmp';
private readonly MAX_FLUSH_TIME = 3000; // 最大flush时间:3秒
private flushAttempts = 0; // flush尝试次数
private readonly MAX_FLUSH_ATTEMPTS = 3; // 最大flush尝试次数
constructor(userDataPath: string) {
if (!userDataPath) {
@@ -169,6 +172,8 @@ export class FileStorageProvider implements IStorageProvider {
});
} catch (error) {
console.error('[FileStorage] Scheduled write failed:', error);
// 重置isDirty标志以避免无限重试
this.isDirty = false;
}
}
this.writeTimeout = null;
@@ -177,18 +182,53 @@ export class FileStorageProvider implements IStorageProvider {
/**
* 立即写入(关键时刻使用)
* 带有超时保护和重试限制,确保不会无限循环
*/
async flush(): Promise<void> {
if (this.writeTimeout) {
clearTimeout(this.writeTimeout);
this.writeTimeout = null;
}
if (this.isDirty) {
await this.acquireWriteLock(async () => {
await this.saveToFile();
if (!this.isDirty) {
return; // 没有脏数据,直接返回
}
// 检查重试次数限制
if (this.flushAttempts >= this.MAX_FLUSH_ATTEMPTS) {
console.error('[FileStorage] Max flush attempts reached, forcing isDirty to false');
this.isDirty = false;
this.flushAttempts = 0;
throw new Error('Max flush attempts exceeded');
}
this.flushAttempts++;
try {
// 使用Promise.race实现超时保护
await Promise.race([
this.acquireWriteLock(async () => {
await this.saveToFile();
this.isDirty = false;
this.flushAttempts = 0; // 成功后重置计数器
console.log('[FileStorage] Data saved successfully');
}),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Flush timeout')), this.MAX_FLUSH_TIME)
)
]);
} catch (error) {
console.error('[FileStorage] Failed to save data during flush:', error);
// 如果达到最大重试次数或者是超时错误,强制重置状态
if (this.flushAttempts >= this.MAX_FLUSH_ATTEMPTS ||
(error instanceof Error && error.message === 'Flush timeout')) {
console.warn('[FileStorage] Forcing isDirty to false to prevent infinite loop');
this.isDirty = false;
});
this.flushAttempts = 0;
}
throw error; // 重新抛出错误以便上层处理
}
}
@@ -1,5 +1,6 @@
import type { Template, ITemplateManager } from './types';
import type { BuiltinTemplateLanguage } from './languageService';
import { safeSerializeForIPC } from '../../utils/ipc-serialization';
// 为window.electronAPI提供完整的类型定义,以确保类型安全
interface ElectronAPI {
@@ -43,7 +44,9 @@ export class ElectronTemplateManagerProxy implements ITemplateManager {
}
async saveTemplate(template: Template): Promise<void> {
return this.electronAPI.createTemplate(template);
// 自动序列化,防止Vue响应式对象IPC传递错误
const safeTemplate = safeSerializeForIPC(template);
return this.electronAPI.createTemplate(safeTemplate);
}
async deleteTemplate(id: string): Promise<void> {
@@ -59,6 +62,7 @@ export class ElectronTemplateManagerProxy implements ITemplateManager {
}
async importTemplate(jsonString: string): Promise<void> {
// jsonString是基本类型,不需要序列化,但为了一致性保留注释
return this.electronAPI.importTemplate(jsonString);
}
@@ -0,0 +1,60 @@
/**
* IPC序列化工具
* 用于处理Vue响应式对象在Electron IPC通信中的序列化问题
*
* 这个工具专门为ElectronProxy类设计,提供统一的序列化处理
*/
/**
* 安全序列化函数,用于清理Vue响应式对象
* 确保所有通过IPC传递的对象都是纯净的JavaScript对象
*
* @param obj 要序列化的对象
* @returns 纯净的JavaScript对象
*/
export function safeSerializeForIPC<T>(obj: T): T {
if (obj === null || obj === undefined) {
return obj;
}
// 对于基本类型,直接返回
if (typeof obj !== 'object') {
return obj;
}
// 使用JSON序列化确保100%的IPC兼容性
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error('[IPC Serialization] Failed to serialize object:', error);
throw new Error(`Failed to serialize object for IPC: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* 检查对象是否可以安全地通过IPC传递
* 主要用于开发时调试
*
* @param obj 要检查的对象
* @param label 对象标签,用于日志输出
*/
export function debugIPCSerializability(obj: any, label: string = 'object'): void {
try {
JSON.stringify(obj);
console.log(`[IPC Debug] ${label} is serializable`);
} catch (error) {
console.error(`[IPC Debug] ${label} is NOT serializable:`, error);
console.error(`[IPC Debug] Object:`, obj);
}
}
/**
* 批量序列化多个参数
* 用于有多个参数需要序列化的场景
*
* @param args 参数数组
* @returns 序列化后的参数数组
*/
export function safeSerializeArgs<T extends any[]>(...args: T): T {
return args.map(arg => safeSerializeForIPC(arg)) as T;
}
+102 -9
View File
@@ -17,6 +17,23 @@ const {
let mainWindow;
let modelManager, templateManager, historyManager, llmService, promptService, templateLanguageService, preferenceService, dataManager;
let storageProvider; // 全局存储提供器引用,用于退出时保存数据
let isQuitting = false; // 防止重复保存数据的标志
let forceQuitTimer = null; // 强制退出定时器
const MAX_SAVE_TIME = 5000; // 最大保存时间:5秒
let emergencyExitTimer = null; // 应急退出定时器
const EMERGENCY_EXIT_TIME = 10000; // 应急退出时间:10秒
// 应急退出机制:无论如何都要在10秒内退出
function setupEmergencyExit() {
if (emergencyExitTimer) {
clearTimeout(emergencyExitTimer);
}
emergencyExitTimer = setTimeout(() => {
console.error('[DESKTOP] EMERGENCY EXIT: Force terminating process after 10 seconds');
process.exit(1); // 强制终止进程
}, EMERGENCY_EXIT_TIME);
}
async function initializePreferenceService(storageProvider) {
console.log('[DESKTOP] Initializing PreferenceService with the provided storage provider...');
@@ -75,17 +92,44 @@ function createWindow() {
// 窗口关闭前保存数据
mainWindow.on('close', async (event) => {
if (storageProvider && typeof storageProvider.flush === 'function') {
if (!isQuitting && storageProvider && typeof storageProvider.flush === 'function') {
event.preventDefault(); // 阻止立即关闭
isQuitting = true; // 设置退出标志
// 启动应急退出机制
setupEmergencyExit();
// 设置强制退出定时器,确保程序不会卡住
forceQuitTimer = setTimeout(() => {
console.warn('[DESKTOP] Force closing window due to timeout');
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.destroy();
}
}, MAX_SAVE_TIME);
try {
console.log('[DESKTOP] Saving data before window close...');
await storageProvider.flush();
await Promise.race([
storageProvider.flush(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Save timeout')), MAX_SAVE_TIME - 1000)
)
]);
console.log('[DESKTOP] Data saved successfully');
mainWindow.destroy(); // 手动关闭窗口
} catch (error) {
console.error('[DESKTOP] Failed to save data before close:', error);
mainWindow.destroy(); // 即使保存失败也要关闭
} finally {
if (forceQuitTimer) {
clearTimeout(forceQuitTimer);
forceQuitTimer = null;
}
if (emergencyExitTimer) {
clearTimeout(emergencyExitTimer);
emergencyExitTimer = null;
}
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.destroy();
}
}
}
});
@@ -158,7 +202,10 @@ async function initializeServices() {
console.log('[DESKTOP] Creating template language service...');
templateLanguageService = createTemplateLanguageService(storageProvider);
console.log('[DESKTOP] Initializing template language service...');
await templateLanguageService.initialize();
console.log('[DESKTOP] Creating template manager...');
templateManager = createTemplateManager(storageProvider, templateLanguageService);
@@ -725,20 +772,66 @@ app.whenReady().then(async () => {
});
});
// 进程信号处理器 - 最后的保障
process.on('SIGINT', () => {
console.log('[DESKTOP] Received SIGINT, forcing exit...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('[DESKTOP] Received SIGTERM, forcing exit...');
process.exit(0);
});
// 捕获未处理的异常,防止程序卡死
process.on('uncaughtException', (error) => {
console.error('[DESKTOP] Uncaught exception:', error);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('[DESKTOP] Unhandled rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// 应用退出前保存数据
app.on('before-quit', async (event) => {
if (storageProvider && typeof storageProvider.flush === 'function') {
if (!isQuitting && storageProvider && typeof storageProvider.flush === 'function') {
event.preventDefault(); // 阻止立即退出
isQuitting = true; // 设置退出标志
// 启动应急退出机制
setupEmergencyExit();
// 设置强制退出定时器,确保应用不会卡住
const forceAppQuitTimer = setTimeout(() => {
console.warn('[DESKTOP] Force quitting app due to timeout');
process.exit(0); // 强制退出进程
}, MAX_SAVE_TIME);
try {
console.log('[DESKTOP] Saving data before quit...');
await storageProvider.flush();
await Promise.race([
storageProvider.flush(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Save timeout')), MAX_SAVE_TIME - 1000)
)
]);
console.log('[DESKTOP] Data saved successfully');
} catch (error) {
console.error('[DESKTOP] Failed to save data before quit:', error);
} finally {
clearTimeout(forceAppQuitTimer);
if (emergencyExitTimer) {
clearTimeout(emergencyExitTimer);
emergencyExitTimer = null;
}
// 使用setImmediate确保在下一个事件循环中退出
setImmediate(() => {
isQuitting = false; // 重置标志以允许正常退出
app.quit(); // 手动退出
});
}
app.quit(); // 手动退出
}
});
+2 -1
View File
@@ -33,7 +33,8 @@
"node_modules/**/*"
],
"win": {
"target": "nsis"
"target": "portable",
"artifactName": "${productName}-${version}.${ext}"
},
"mac": {
"target": "dmg"
+7 -5
View File
@@ -500,6 +500,7 @@ import {
import { useToast } from '../composables/useToast';
import InputWithSelect from './InputWithSelect.vue'
const { t } = useI18n()
const toast = useToast();
const emit = defineEmits(['modelsUpdated', 'close', 'select', 'update:show']);
@@ -949,20 +950,20 @@ const saveEdit = async () => {
const originalKey = editingModel.value.originalKey;
// 创建更新配置对象
// 创建更新配置对象,ElectronProxy会自动处理序列化
const config = {
name: editingModel.value.name,
baseURL: editingModel.value.baseURL,
// 如果是掩码密钥,使用原始密钥;否则使用新输入的密钥
apiKey: editingModel.value.displayMaskedKey
? editingModel.value.originalApiKey
apiKey: editingModel.value.displayMaskedKey
? editingModel.value.originalApiKey
: editingModel.value.apiKey,
defaultModel: editingModel.value.defaultModel,
models: [editingModel.value.defaultModel], // 可以根据需要扩展
useVercelProxy: editingModel.value.useVercelProxy,
provider: editingModel.value.provider || 'custom',
enabled: editingModel.value.enabled !== undefined
? editingModel.value.enabled
enabled: editingModel.value.enabled !== undefined
? editingModel.value.enabled
: true,
llmParams: editingModel.value.llmParams || {}
};
@@ -991,6 +992,7 @@ const saveEdit = async () => {
// 添加自定义模型
const addCustomModel = async () => {
try {
// ElectronProxy会自动处理序列化
const config = {
name: newModel.value.name,
baseURL: newModel.value.baseURL,
@@ -788,7 +788,7 @@ const processedPreview = computed(() => {
const tempTemplate: Template = {
id: 'preview',
name: 'Preview',
content: form.value.messages,
content: JSON.parse(JSON.stringify(form.value.messages)),
metadata: { version: '1.0', lastModified: Date.now(), templateType: currentType.value }
}
return TemplateProcessor.processTemplate(tempTemplate, sampleContext)
@@ -982,6 +982,7 @@ const applyMigration = async () => {
}
}
// ElectronProxy会自动处理序列化,这里不需要手动处理
await getTemplateManager.value.saveTemplate(updatedTemplate)
await loadTemplates()
@@ -1041,10 +1042,11 @@ const handleSubmit = async () => {
const templateData: Template = {
id: editingTemplate.value?.id || generateUniqueTemplateId('user-template'),
name: form.value.name,
content: form.value.isAdvanced ? form.value.messages : form.value.content,
content: form.value.isAdvanced ? JSON.parse(JSON.stringify(form.value.messages)) : form.value.content,
metadata
}
// IPC层会自动处理序列化,这里不需要手动处理
await getTemplateManager.value.saveTemplate(templateData)
await loadTemplates()
@@ -1,6 +1,7 @@
import { ref, watch, computed, reactive, type Ref } from 'vue'
import { useToast } from './useToast'
import { useI18n } from 'vue-i18n'
import { v4 as uuidv4 } from 'uuid'
import type { IHistoryManager, PromptRecordChain, PromptRecord } from '@prompt-optimizer/core'
import type { AppServices } from '../types/services'
@@ -38,11 +39,12 @@ export function usePromptHistory(
handleSelectHistory: async (context: { record: any, chainId: string, rootPrompt: string }) => {
const { record, rootPrompt } = context
prompt.value = rootPrompt
optimizedPrompt.value = record.optimizedPrompt
const newRecord = await historyManager.value!.createNewChain({
// ElectronProxy会自动处理序列化
const newRecordData = {
id: uuidv4(),
originalPrompt: rootPrompt,
optimizedPrompt: record.optimizedPrompt,
@@ -53,7 +55,9 @@ export function usePromptHistory(
metadata: {
optimizationMode: record.metadata?.optimizationMode // 保持原始记录的优化模式
}
})
}
const newRecord = await historyManager.value!.createNewChain(newRecordData)
currentChainId.value = newRecord.chainId
currentVersions.value = newRecord.versions
@@ -1,6 +1,7 @@
import { ref, watch, nextTick, computed, reactive } from 'vue'
import { useToast } from './useToast'
import { useI18n } from 'vue-i18n'
import { v4 as uuidv4 } from 'uuid'
import type { Ref } from 'vue'
import type {
@@ -122,8 +123,8 @@ export function usePromptOptimizer(
if (!currentTemplate) return
try {
// Create new record chain with enhanced metadata
const newRecord = await historyManager.value!.createNewChain({
// Create new record chain with enhanced metadata,ElectronProxy会自动处理序列化
const recordData = {
id: uuidv4(),
originalPrompt: state.prompt,
optimizedPrompt: state.optimizedPrompt,
@@ -134,7 +135,9 @@ export function usePromptOptimizer(
metadata: {
optimizationMode: optimizationMode.value
}
});
};
const newRecord = await historyManager.value!.createNewChain(recordData);
state.currentChainId = newRecord.chainId;
state.currentVersions = newRecord.versions;
@@ -199,15 +202,17 @@ export function usePromptOptimizer(
}
try {
// 使用正确的addIteration方法来保存迭代历史
const updatedChain = await historyManager.value!.addIteration({
// 使用正确的addIteration方法来保存迭代历史,ElectronProxy会自动处理序列化
const iterationData = {
chainId: state.currentChainId,
originalPrompt: originalPrompt,
optimizedPrompt: state.optimizedPrompt,
iterationNote: iterateInput,
modelKey: optimizeModel.value,
templateId: state.selectedIterateTemplate.id
});
};
const updatedChain = await historyManager.value!.addIteration(iterationData);
state.currentVersions = updatedChain.versions
state.currentVersionId = updatedChain.currentRecord.id
-11
View File
@@ -1,11 +0,0 @@
{
"pref:theme-id": "\"dark\"",
"pref:app:selected-optimize-model": "\"gemini\"",
"pref:app:selected-optimize-template": "\"general-optimize\"",
"pref:app:selected-user-optimize-template": "\"user-prompt-professional\"",
"pref:app:selected-iterate-template": "\"iterate\"",
"pref:app:selected-test-model": "\"gemini\"",
"builtin-template-language": "zh-CN",
"prompt_history": "[{\"id\":\"2ded8a19-3939-45dd-80b9-fed914af9ce4\",\"originalPrompt\":\"你是一个诗人\",\"optimizedPrompt\":\"# Role: 诗人\\n\\n## Profile\\n- language: 中文\\n- description: 你是一位富有创造力和想象力的诗人,擅长运用各种诗歌形式,捕捉生活中的美好、感悟和哲理,并将它们转化为优美动人的诗歌。\\n- background: 你可能经历了丰富的人生阅历,对自然、社会、人文等方面有着深刻的理解和感悟。你的作品可能受到古典诗歌、现代诗歌以及其他艺术形式的影响。\\n- personality: 敏感细腻、情感丰富、观察力强、充满想象力、富有创造力。\\n- expertise: 诗歌创作、诗歌鉴赏、文学理论、语言运用。\\n- target_audience: 诗歌爱好者、文学研究者、普通读者。\\n\\n## Skills\\n\\n1. 诗歌创作\\n - 格律诗创作: 能够熟练运用各种格律诗的创作技巧,如对仗、押韵、平仄等。\\n - 现代诗创作: 能够自由地表达情感和思想,运用意象、象征、隐喻等手法。\\n - 散文诗创作: 能够将散文的叙述性和诗歌的意境相结合。\\n - 主题提炼: 能够从生活体验中提炼出具有普遍意义的主题。\\n\\n2. 语言运用\\n - 词语选择: 能够精准地选择词语,表达细腻的情感和丰富的意境。\\n - 修辞手法: 能够灵活运用各种修辞手法,增强诗歌的表现力和感染力。\\n - 节奏韵律: 能够把握诗歌的节奏韵律,使诗歌更具音乐性。\\n - 意象构建: 能够创造出鲜明生动的意象,引发读者的联想和共鸣。\\n\\n## Rules\\n\\n1. 基本原则:\\n - 情感真挚: 诗歌应表达真挚的情感,避免空洞和虚假。\\n - 意象鲜明: 运用生动形象的意象,增强诗歌的感染力。\\n - 语言精炼: 运用简洁精炼的语言,表达深刻的思想。\\n - 主题深刻: 诗歌应具有一定的思想深度,引发读者的思考。\\n\\n2. 行为准则:\\n - 尊重原创: 不抄袭他人的作品,坚持原创。\\n - 不断学习: 不断学习诗歌理论和创作技巧,提升自身水平。\\n - 广泛阅读: 广泛阅读各种文学作品,拓展知识面。\\n - 虚心接受: 虚心接受他人的意见和建议,不断改进自己的作品。\\n\\n3. 限制条件:\\n - 避免政治敏感: 避免创作涉及政治敏感话题的诗歌。\\n - 避免低俗内容: 避免创作内容低俗、格调不高的诗歌。\\n - 避免过度晦涩: 避免使用过于晦涩难懂的语言,影响诗歌的传播。\\n - 避免过度堆砌辞藻: 避免过度使用华丽的辞藻,使诗歌显得空洞无物。\\n\\n## Workflows\\n\\n- 目标: 根据用户给定的主题或情境,创作一首高质量的诗歌。\\n- 步骤 1: 仔细理解用户提供的主题或情境,提炼出诗歌的核心情感和思想。\\n- 步骤 2: 构思诗歌的结构和布局,确定采用何种诗歌形式。\\n- 步骤 3: 选择恰当的意象和语言,运用修辞手法,进行诗歌的创作。\\n- 步骤 4: 对诗歌进行反复修改和润色,力求使其更加完美。\\n- 预期结果: 创作出一首主题鲜明、情感真挚、语言精炼、意象生动的诗歌,能够引发读者的共鸣和思考。\\n\\n## Initialization\\n作为诗人,你必须遵守上述Rules,按照Workflows执行任务。\\n\",\"type\":\"optimize\",\"modelKey\":\"gemini\",\"templateId\":\"general-optimize\",\"timestamp\":1751778053458,\"metadata\":{\"optimizationMode\":\"system\"},\"chainId\":\"8450c0bf-3c93-4920-a9bb-da9429a8a417\",\"version\":1,\"modelName\":\"Gemini\"}]",
"user-templates": "[{\"id\":\"user-template-1751778165441-s6f1pd\",\"name\":\"通用优化 - 副本\",\"content\":\"你是一个专业的AI提示词优化专家。请帮我优化以下prompt,并按照以下格式返回:\\n\\n# Role: [角色名称]\\n\\n## Profile\\n- language: [语言]\\n- description: [详细的角色描述]\\n- background: [角色背景]\\n- personality: [性格特征]\\n- expertise: [专业领域]\\n- target_audience: [目标用户群]\\n\\n## Skills\\n\\n1. [核心技能类别]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n\\n2. [辅助技能类别]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n - [具体技能]: [简要说明]\\n\\n## Rules\\n\\n1. [基本原则]:\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n\\n2. [行为准则]:\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n - [具体规则]: [详细说明]\\n\\n3. [限制条件]:\\n - [具体限制]: [详细说明]\\n - [具体限制]: [详细说明]\\n - [具体限制]: [详细说明]\\n - [具体限制]: [详细说明]\\n\\n## Workflows\\n\\n- 目标: [明确目标]\\n- 步骤 1: [详细说明]\\n- 步骤 2: [详细说明]\\n- 步骤 3: [详细说明]\\n- 预期结果: [说明]\\n\\n\\n## Initialization\\n作为[角色名称],你必须遵守上述Rules,按照Workflows执行任务。\\n\\n\\n请基于以上模板,优化并扩展以下prompt,确保内容专业、完整且结构清晰,注意不要携带任何引导词或解释,不要使用代码块包围:\\n \",\"metadata\":{\"version\":\"1.0.0\",\"lastModified\":1751778165441,\"description\":\"通用优化提示词,适用于大多数场景\",\"author\":\"User\",\"templateType\":\"optimize\"},\"isBuiltin\":false}]"
}