diff --git a/docs/archives/115-ipc-serialization-fixes/architecture-evolution.md b/docs/archives/115-ipc-serialization-fixes/architecture-evolution.md index c7aaeaa9..22a43bd5 100644 --- a/docs/archives/115-ipc-serialization-fixes/architecture-evolution.md +++ b/docs/archives/115-ipc-serialization-fixes/architecture-evolution.md @@ -146,3 +146,56 @@ await modelManager.addModel(key, config) // 自动序列化 - ❌ 忽视开发体验 这次架构演进是一个很好的例子,展示了如何通过合理的架构设计来解决技术问题,同时提升开发体验。 + +## 🔄 第三阶段:代理层职责边界优化 (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; + + // 修复后 + fetchModelList: (provider: string, customConfig?: any) => Promise>; + ``` + +2. **简化代理层**: + ```typescript + // ElectronProxy只负责IPC通信,不做数据转换 + async fetchModelList(provider: string, customConfig?: Partial): Promise { + const safeCustomConfig = customConfig ? safeSerializeForIPC(customConfig) : customConfig; + return this.electronAPI.llm.fetchModelList(provider, safeCustomConfig); + } + ``` + +3. **移除冗余事件**:删除不必要的`@select`事件处理,简化数据流 + +### 架构原则确立 +- **单一职责**:每层只负责自己的核心功能,代理层专注IPC通信 +- **类型安全**:TypeScript类型定义必须与实际实现保持严格一致 +- **数据流简洁**:避免不必要的中间转换层,减少出错可能性 + +### 经验总结 +1. **异步性是双刃剑**:它会放大架构设计中的潜在问题 +2. **类型安全的重要性**:类型定义不仅是文档,更是架构约束 +3. **职责边界要清晰**:特别是在跨进程通信的场景下 +4. **环境差异要重视**:同样的代码在不同环境下可能表现不同 + +这次经验强化了我们对IPC架构设计的理解,为未来的跨进程功能开发提供了重要指导。 diff --git a/packages/core/src/services/llm/electron-proxy.ts b/packages/core/src/services/llm/electron-proxy.ts index b7d5eaaa..919e7417 100644 --- a/packages/core/src/services/llm/electron-proxy.ts +++ b/packages/core/src/services/llm/electron-proxy.ts @@ -57,8 +57,6 @@ export class ElectronLLMProxy implements ILLMService { ): Promise { // 自动序列化,防止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 })); + return this.electronAPI.llm.fetchModelList(provider, safeCustomConfig); } } \ No newline at end of file diff --git a/packages/core/src/types/global.d.ts b/packages/core/src/types/global.d.ts index 49a6b02e..b8f291f9 100644 --- a/packages/core/src/types/global.d.ts +++ b/packages/core/src/types/global.d.ts @@ -26,7 +26,7 @@ interface Window { } ) => Promise; testConnection: (provider: string) => Promise; - fetchModelList: (provider: string, customConfig?: any) => Promise; + fetchModelList: (provider: string, customConfig?: any) => Promise>; }; model: { getModels: () => Promise; diff --git a/packages/ui/src/components/ModelManager.vue b/packages/ui/src/components/ModelManager.vue index ba75fad9..6938ae83 100644 --- a/packages/ui/src/components/ModelManager.vue +++ b/packages/ui/src/components/ModelManager.vue @@ -145,7 +145,6 @@ :hint-text="t('modelManager.clickToFetchModels')" required :placeholder="t('modelManager.defaultModelPlaceholder')" - @select="handleModelSelect" @fetch-options="handleFetchEditingModels" /> @@ -339,7 +338,6 @@ :hint-text="t('modelManager.clickToFetchModels')" required :placeholder="t('modelManager.defaultModelPlaceholder')" - @select="handleModelSelect" @fetch-options="handleFetchNewModels" /> @@ -715,7 +713,7 @@ const editModel = async (key) => { originalKey: key, // 保存原始key name: model.name, baseURL: model.baseURL, - defaultModel: model.defaultModel, + defaultModel: typeof model.defaultModel === 'string' ? model.defaultModel : (model.defaultModel?.value || model.defaultModel?.id || ''), apiKey: maskedApiKey, displayMaskedKey: true, originalApiKey: model.apiKey, @@ -808,10 +806,7 @@ const addDefaultModelOptions = (providerKey) => { modelOptions.value = []; } }; -const handleModelSelect = (option) => { - // This can be expanded if additional logic is needed when model is selected - console.log('Selected model:', option); -}; + const handleFetchEditingModels = async () => { if (!editingModel.value) { return; @@ -858,7 +853,7 @@ const handleFetchEditingModels = async () => { if (models.length > 0) { modelOptions.value = models; toast.success(t('modelManager.fetchModelsSuccess', {count: models.length})); - + // 如果当前选择的模型不在列表中,默认选择第一个 if (!models.some(m => m.value === editingModel.value.defaultModel)) { editingModel.value.defaultModel = models[0].value; @@ -916,7 +911,7 @@ const handleFetchNewModels = async () => { if (models.length > 0) { modelOptions.value = models; toast.success(t('modelManager.fetchModelsSuccess', {count: models.length})); - + // 默认选择第一个模型 newModel.value.defaultModel = models[0].value; } else {