From 2f4d7429efd85feaee603723c8a146f66c12a3bc Mon Sep 17 00:00:00 2001 From: linshen <32978552+linshenkx@users.noreply.github.com> Date: Sat, 28 Jun 2025 19:43:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=8E=E7=BB=84=E4=BB=B6=EF=BC=8C=E5=BC=95=E5=85=A5=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=88=9D=E5=A7=8B=E5=8C=96=E5=99=A8=E4=B8=8E=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E6=88=90Vue=20Composable=E6=9E=B6=E6=9E=84=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 `composables-refactor-plan.md` 与 `experience.md`,记录重构成果、问题与后续计划,强调顶层声明和响应式连接。引入 `useAppInitializer`,及其对架构统一和代码复用的贡献。 - 更新多个组件与Composable文件,使其通过 `inject` 获取服务实例引用,解决了`ref` 迁移到 `reactive` 和异步回调中调用Composable函数导致的错误,优化了模板管理器的使用方式,并移除对服务单例的直接依赖。 - 修复 `templateLanguageService` 依赖注入失败的 bug,更新组件导出和模板管理器,优化代码结构与可读性。 - 将背景与目标修改为当前状态与问题,明确上层应用尚未适配底层重构的现状,并记录Web应用和浏览器插件启动失败问题,提出后续修复步骤。更新实施计划,强调对齐上层应用与底层服务架构的必要性。 --- docs/composables-refactor-plan.md | 108 +++ docs/desktop-architecture-refactor-plan.md | 50 +- docs/experience.md | 141 ++++ docs/project-status.md | 8 + docs/scratchpad.md | 169 ++++- docs/singleton-refactor-plan.md | 127 ++-- docs/web-refactor-plan.md | 94 +++ packages/core/src/index.ts | 19 +- packages/core/src/services/compare/service.ts | 8 + packages/core/src/services/data/manager.ts | 107 ++- packages/core/src/services/history/manager.ts | 17 +- .../core/src/services/llm/electron-proxy.ts | 8 - packages/core/src/services/llm/service.ts | 19 +- packages/core/src/services/model/manager.ts | 1 - .../src/services/prompt/electron-proxy.ts | 86 +++ packages/core/src/services/prompt/service.ts | 7 +- packages/core/src/services/prompt/types.ts | 4 +- .../src/services/template/electron-proxy.ts | 5 + .../src/services/template/languageService.ts | 2 +- .../core/src/services/template/manager.ts | 1 - packages/core/src/services/template/types.ts | 3 + packages/core/src/utils/environment.ts | 109 +-- .../core/tests/integration/llm/openai.test.js | 14 +- .../tests/integration/real-components.test.ts | 18 +- packages/core/tests/unit/data/manager.test.ts | 619 ++++------------- .../core/tests/unit/llm/llmParams.test.ts | 6 +- .../core/tests/unit/model/manager.test.ts | 38 +- .../unit/template/advanced-optimize.test.ts | 4 +- .../core/tests/unit/template/manager.test.ts | 486 +++---------- packages/extension/src/App.vue | 652 ++++++++---------- .../BuiltinTemplateLanguageSwitch.vue | 89 ++- packages/ui/src/components/DataManager.vue | 38 +- packages/ui/src/components/HistoryDrawer.vue | 1 - packages/ui/src/components/LanguageSwitch.vue | 6 +- packages/ui/src/components/ModelManager.vue | 31 +- packages/ui/src/components/ModelSelect.vue | 34 +- .../ui/src/components/OutputDisplayCore.vue | 15 +- packages/ui/src/components/PromptPanel.vue | 8 +- .../ui/src/components/TemplateManager.vue | 33 +- packages/ui/src/components/TemplateSelect.vue | 130 ++-- packages/ui/src/components/ThemeToggleUI.vue | 5 +- packages/ui/src/composables/index.ts | 6 +- .../ui/src/composables/useAppInitializer.ts | 138 ++++ .../ui/src/composables/useHistoryManager.ts | 69 +- packages/ui/src/composables/useModals.ts | 103 +-- .../ui/src/composables/useModelManager.ts | 223 +++--- .../ui/src/composables/useModelSelectors.ts | 8 +- .../ui/src/composables/usePromptHistory.ts | 81 ++- .../ui/src/composables/usePromptOptimizer.ts | 260 ++++--- .../ui/src/composables/usePromptTester.ts | 244 ++++--- .../src/composables/useServiceInitializer.ts | 85 --- packages/ui/src/composables/useStorage.ts | 33 +- .../ui/src/composables/useTemplateManager.ts | 165 ++--- packages/ui/src/i18n/locales/en-US.ts | 4 +- packages/ui/src/i18n/locales/zh-CN.ts | 4 +- packages/ui/src/index.ts | 37 +- packages/ui/src/types/services.ts | 24 + packages/web/package.json | 2 +- packages/web/src/App.vue | 652 ++++++++---------- 59 files changed, 2792 insertions(+), 2666 deletions(-) create mode 100644 docs/composables-refactor-plan.md create mode 100644 docs/web-refactor-plan.md create mode 100644 packages/core/src/services/prompt/electron-proxy.ts create mode 100644 packages/ui/src/composables/useAppInitializer.ts delete mode 100644 packages/ui/src/composables/useServiceInitializer.ts create mode 100644 packages/ui/src/types/services.ts diff --git a/docs/composables-refactor-plan.md b/docs/composables-refactor-plan.md new file mode 100644 index 00000000..c9502d5c --- /dev/null +++ b/docs/composables-refactor-plan.md @@ -0,0 +1,108 @@ +# Vue Composable 架构重构计划 + +## 1. 背景与问题 + +在对核心服务进行“去单例化”重构后,应用启动时暴露出一系列与 Vue 响应式系统和组件通信相关的严重问题。这些问题起初表现为多种警告和错误: + +1. **属性类型不匹配**: 子组件收到的 props 类型与预期不符,例如期望 `Boolean` 却收到了 `Object` (`[Vue warn]: Invalid prop: type check failed`)。此问题在 `PromptPanel` 等多个组件中普遍存在。 +2. **无效的侦听源**: `useStorage` 等 Composable 因其依赖的 `services` 对象尚未初始化,导致 `watch` 侦听了一个 `undefined` 源 (`[Vue warn]: Invalid watch source: undefined`)。 +3. **顶层调用错误**: 在某些异步初始化逻辑中尝试调用 Composable,导致 Vue 抛出 `Must be called at the top of a 'setup' function` 错误。 + +## 2. 根本原因分析 + +经过排查,这些看似分散的问题,都指向同一个系统性的架构缺陷:**Composable 的状态封装模式不当**。 + +许多业务逻辑 Composable(如 `usePromptOptimizer`, `useModelManager`)返回的是一个包含了多个 `ref` 的普通 JavaScript 对象,形如: + +```typescript +// 旧模式 +function usePromptOptimizer() { + const isIterating = ref(false); + const someOtherState = ref(''); + return { isIterating, someOtherState }; +} +``` + +当在 `App.vue` 中使用时: + +```html + + + + +``` + +`PromptPanel` 组件收到的 `is-iterating` prop 是一个 `Ref` 对象,而非期望的 `boolean` 值,导致类型检查失败。这个问题是所有连锁反应的核心。 + +## 3. 解决方案:统一返回 `reactive` 对象 + +为了从根本上解决问题,我们采取了统一的架构决策:**重构所有核心业务 Composable,使其返回一个单一的 `reactive` 对象**。 + +```typescript +// ✅ 新模式 +function usePromptOptimizer() { + const state = reactive({ + isIterating: false, + someOtherState: '', + }); + + // ... 逻辑代码修改 state ... + + return state; // 返回一个响应式对象 +} +``` + +当在 `App.vue` 中使用时,问题迎刃而解: + +```html + + + + +``` + +这个模式确保了传递给子组件的是原始值,而不是 `ref` 包装器,同时保留了跨组件的状态响应性。 + +## 4. 实施过程与成果 (已完成) ✅ + +本次重构已**圆满完成**。 + +**核心重构**: +- [x] **`usePromptOptimizer`**: 已重构为返回 `reactive` 对象。 +- [x] **`useModelManager`**: 已重构为返回 `reactive` 对象。 +- [x] **`useHistoryManager`**: 已重构为返回 `reactive` 对象。 +- [x] **`useTemplateManager`**: 已重构为返回 `reactive` 对象。 +- [x] **`usePromptTester`**: 已重构为返回 `reactive` 对象。 +- [x] **`useModals`**: 已重构为返回 `reactive` 对象。 + +**辅助修复**: +- [x] **修复 `useStorage`**: `ThemeToggleUI` 和 `LanguageSwitch` 组件被修改为通过 `inject` 获取 `services` 实例,并将其传递给 `useStorage`,解决了依赖过早初始化的问题。 +- [x] **适配 `App.vue`**: 调整了 `App.vue` 中的模板绑定和 `computed` 属性,以适应新的 `reactive` 状态结构,并修复了因此产生的类型错误。 +- [x] **依赖注入**: 在 `ModelSelect` 和 `DataManager` 等组件中,推广了使用 `inject` 直接从 `services` 中获取依赖的模式,简化了 `App.vue` 的模板。 + +**最终成果**: +- 彻底解决了启动时的所有 Vue `warn` 和 `error`。 +- 建立了一套更健壮、更可预测、更符合 Vue 最佳实践的状态管理范式。 +- 应用代码,特别是 `App.vue`,变得更加简洁和易于维护。 + +## 5. 经验总结 + +- **`reactive` vs. 对象包裹的 `ref`**: 对于一组高度内聚、会被一同传递或操作的响应式状态,使用 `reactive` 封装是比返回一个包含多个 `ref` 的对象更优的模式。它能有效避免深层解包问题,并简化消费端的代码。 +- **`provide`/`inject` 是服务注入的利器**: 对于全局性或跨层级的服务/依赖(如 `services` 对象),使用 `provide`/`inject` 是比层层传递 `props` 更优雅、更高效的解决方案。 +- **系统性问题需要系统性解决方案**: 面对一系列看似不同的报错,深入分析其共性根源至关重要。本次通过识别核心的“状态封装模式”问题,一次性解决了所有表层症状。 \ No newline at end of file diff --git a/docs/desktop-architecture-refactor-plan.md b/docs/desktop-architecture-refactor-plan.md index 947e412d..4561e4b4 100644 --- a/docs/desktop-architecture-refactor-plan.md +++ b/docs/desktop-architecture-refactor-plan.md @@ -37,9 +37,15 @@ - **文件**: `packages/core/src/index.ts` ✅ - **操作**: 导出 `MemoryStorageProvider` 类 ✅ +#### 3. 创建工厂函数 ✅ +- **文件**: `packages/core/src/services/storage/factory.ts` ✅ +- **操作**: 在 `StorageFactory.create()` 中添加 `'memory'` 选项 ✅ +- **文件**: `packages/core/src/index.ts` ✅ +- **操作**: 导出 `MemoryStorageProvider` 类 ✅ + ### 阶段二:后端改造 (主进程) -#### 3. 清理并重构主进程 +#### 4. 清理并重构主进程 - **文件**: `packages/desktop/main.js` - **删除内容**: - 所有 `ipcMain.handle('api-fetch', ...)` 处理器 @@ -50,7 +56,7 @@ - 使用 `StorageFactory.create('memory')` 创建存储实例 - 实例化所有核心服务 (`ModelManager`, `TemplateManager`, etc.) -#### 4. 建立高层服务 IPC 接口 +#### 5. 建立高层服务 IPC 接口 - **文件**: `packages/desktop/main.js` - **接口清单**: ```javascript @@ -82,7 +88,7 @@ ### 阶段三:通信与前端改造 -#### 5. 重构预加载脚本 +#### 6. 重构预加载脚本 - **文件**: `packages/desktop/preload.js` - **删除内容**: 所有 `fetch` 拦截和模拟逻辑 - **新增内容**: 结构化的 `electronAPI` 对象 @@ -102,7 +108,7 @@ }); ``` -#### 6. 创建渲染进程服务代理类 +#### 7. 创建渲染进程服务代理类 - **目标**: 为每个核心服务创建 Electron 代理类 - **文件清单**: - `packages/core/src/services/model/electron-proxy.ts` @@ -111,21 +117,14 @@ - `packages/core/src/services/prompt/electron-proxy.ts` - **要求**: 每个代理类实现对应服务的接口,内部调用 `window.electronAPI` -#### 7. 改造UI服务初始化逻辑 -- **文件**: `packages/ui/src/composables/useServiceInitializer.ts` -- **逻辑**: +#### 8. 改造UI服务初始化逻辑 +- **文件**: `packages/ui/src/composables/useAppInitializer.ts` +- **逻辑**: `useAppInitializer` 会自动检测运行环境。 ```typescript - if (isRunningInElectron()) { - // 使用代理类 - const modelManager = new ElectronModelManagerProxy(); - const templateManager = new ElectronTemplateManagerProxy(); - // ... - } else { - // 使用真实服务类 - const storageProvider = StorageFactory.create('dexie'); - const modelManager = new ModelManager(storageProvider); - const templateManager = new TemplateManager(storageProvider); - // ... + if (isRunningInElectron()) { // Electron 环境 + // 初始化所有代理服务... + } else { // Web 环境 + // 初始化所有真实服务... } ``` @@ -196,18 +195,23 @@ - `createHistoryManager()` 工厂函数 - 所有工厂函数正确导出 +4. **✅ 接口完善与代理适配** + - 在 `ITemplateManager` 接口中添加 `isInitialized()` 方法 + - 在 `ElectronTemplateManagerProxy` 类中实现 `isInitialized()` 方法 + - 确保所有代理类正确实现了对应的接口 + #### 阶段二:后端改造 (主进程) - 100% 完成 -4. **✅ 重构 main.js** +5. **✅ 重构 main.js** - 使用 `MemoryStorageProvider` 替代 `LocalStorageProvider` - 实现完整的高层 IPC 服务接口 - 支持 LLM、Model、Template、History 所有服务 -5. **✅ 更新 preload.js** +6. **✅ 更新 preload.js** - 提供完整的 `electronAPI` 接口 - 支持所有核心服务的 IPC 通信 - 正确的错误处理和类型安全 -6. **✅ 创建代理类** +7. **✅ 创建代理类** - `ElectronLLMProxy` 适配 IPC 接口 - `ElectronModelManagerProxy` 实现模型管理 - 更新全局类型定义 @@ -238,4 +242,6 @@ | **代码复用** | ❌ 重复的代理逻辑 | ✅ 主进程直接消费 core 包 | | **类型安全** | ❌ 复杂的类型适配 | ✅ 完整的 TypeScript 支持 | -**最后更新**: 2024年12月28日 \ No newline at end of file +**架构结论**: 本次重构已**圆满完成**。随着统一初始化器 `useAppInitializer` 的引入和应用,桌面端的"高层服务代理"架构已完全落地,实现了各平台间架构的统一和代码的高度复用。 + +**最后更新**: 2024年12月29日 \ No newline at end of file diff --git a/docs/experience.md b/docs/experience.md index 66fe2c31..609c46b0 100644 --- a/docs/experience.md +++ b/docs/experience.md @@ -463,3 +463,144 @@ if (updatedTemplate && contentChanged) { /* ... etc ... */ ``` 通过这种方式,我们既保留了优美的排版,又确保了自定义主题的颜色能够正确渲染。 + +## 8. Vue Composable 架构重构:解决异步初始化问题 + +### 问题背景 +在异步回调中调用Vue Composable函数会导致错误:`Uncaught (in promise) SyntaxError: Must be called at the top of a 'setup' function`。这违反了Vue Composition API的核心规则,需要重构架构。 + +### 核心解决方案:顶层声明,响应式连接,内部自治 +```typescript +// ❌ 错误:在异步回调中调用Composable +onMounted(async () => { + const services = await initServices(); + const modelManager = useModelManager(); // 错误:不在setup顶层调用 +}); + +// ✅ 正确:顶层声明,响应式连接 +const { services } = useAppInitializer(); // 在顶层调用 +const modelManager = useModelManager(services); // 在顶层调用,传入services引用 + +// 内部实现:响应式连接 +export function useModelManager(services: Ref) { + // 状态定义... + + // 响应式连接:监听服务就绪 + watch(services, (newServices) => { + if (!newServices) return; + // 使用已就绪的服务... + }, { immediate: true }); + + return { /* 返回状态和方法 */ }; +} +``` + +### 架构设计要点 +1. **统一服务接口**:创建`AppServices`接口,统一管理所有核心服务 + ```typescript + // packages/ui/src/types/services.ts + export interface AppServices { + storageProvider: IStorageProvider; + modelManager: IModelManager; + templateManager: ITemplateManager; + historyManager: IHistoryManager; + dataManager: DataManager; + llmService: ILLMService; + promptService: IPromptService; + } + ``` + +2. **服务初始化器**:`useAppInitializer`负责创建和初始化所有服务 + ```typescript + // 返回带有错误处理的服务引用 + export function useAppInitializer() { + const services = ref(null); + const isInitializing = ref(true); + const error = ref(null); + + onMounted(async () => { + try { + // 初始化服务... + services.value = { /* 服务实例 */ }; + } catch (err) { + error.value = err instanceof Error ? err : new Error(String(err)); + } finally { + isInitializing.value = false; + } + }); + + return { services, isInitializing, error }; + } + ``` + +3. **Composable参数模式**:所有Composable接收`services`引用作为参数 + ```typescript + export function useModelManager(services: Ref) { + // ... + } + + export function useTemplateManager(services: Ref) { + // ... + } + ``` + +### 关键经验 +1. **Vue响应式上下文**: Vue Composable必须在` -// 初始化历史记录管理器 -const { - history, - handleSelectHistory: handleSelectHistoryBase, - handleClearHistory: handleClearHistoryBase, - handleDeleteChain: handleDeleteChainBase -} = usePromptHistory( - historyManager, - prompt, - optimizedPrompt, - currentChainId, - currentVersions, - currentVersionId -) - -// 初始化历史记录管理器UI -const { - showHistory, - handleSelectHistory, - handleClearHistory, - handleDeleteChain -} = useHistoryManager( - historyManager, - prompt, - optimizedPrompt, - currentChainId, - currentVersions, - currentVersionId, - handleSelectHistoryBase, - handleClearHistoryBase, - handleDeleteChainBase -) - -// Template Manager state -const showTemplates = ref(false) -const currentType = ref('') - -const openTemplateManager = (type: string) => { - currentType.value = type - showTemplates.value = true + \ No newline at end of file diff --git a/packages/ui/src/components/BuiltinTemplateLanguageSwitch.vue b/packages/ui/src/components/BuiltinTemplateLanguageSwitch.vue index f2d16801..64653f13 100644 --- a/packages/ui/src/components/BuiltinTemplateLanguageSwitch.vue +++ b/packages/ui/src/components/BuiltinTemplateLanguageSwitch.vue @@ -45,15 +45,59 @@ \ No newline at end of file