From 98507e63ec583c92cf60b66e218412711991a4c0 Mon Sep 17 00:00:00 2001 From: linshen <32978552+linshenkx@users.noreply.github.com> Date: Sun, 6 Jul 2025 13:02:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96ElectronProxy?= =?UTF-8?q?=E5=B1=82=E8=81=8C=E8=B4=A3=EF=BC=8C=E4=BF=AE=E5=A4=8DfetchMode?= =?UTF-8?q?lList=E7=B1=BB=E5=9E=8B=E5=AE=9A=E4=B9=89=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E6=B5=81=E4=B8=8E=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture-evolution.md | 53 +++++++++++++++++++ .../core/src/services/llm/electron-proxy.ts | 4 +- packages/core/src/types/global.d.ts | 2 +- packages/ui/src/components/ModelManager.vue | 13 ++--- 4 files changed, 59 insertions(+), 13 deletions(-) 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 {