# zn-ai 对话功能重构迁移规划(对齐 ClawX 网关架构) > 目标:彻底抛弃 zn-ai 现有的「WebSocket 直连云端后端」对话模型对接方式,全面对齐 ClawX 的「本地 Gateway Service」架构,使对话调用链路完全由本地网关接管。 --- ## 一、问题诊断与架构差异 ### 1.1 当前现象 - Agents 页面成功添加 DeepSeek 自定义模型; - 对话页面提问「你是什么模型?」; - 返回内容自称「阿里百炼千问」。 ### 1.2 根因分析 **zn-ai 对话系统与模型管理完全割裂:** | 维度 | Agents 模型管理 | 对话功能 (Chat) | |------|----------------|-----------------| | **存储位置** | 本地 JSON (`provider-accounts.json` + `provider-keys.json`) | 无本地模型概念 | | **调用链路** | `hostapi:fetch` -> `providerApiService` | WebSocket -> `wss://onefeel.brother7.cn` | | **模型决策** | 前端本地维护账户列表 | 远端后端根据固定 `agentId` 决定 | | **鉴权方式** | 用户填写的 API Key | 后端统一配置的千问密钥 | **结论**:对话消息从未读取本地 provider 配置,而是直接发给 zn-ai 云端后端;该后端绑定的默认模型就是阿里百炼千问。 ### 1.3 与 ClawX 的核心架构差异 **ClawX 架构:** ``` Renderer Chat Store │ invokeIpc('gateway:rpc', 'chat.send', { sessionKey, message, ... }) ▼ Electron Main: GatewayManager │ 管理 OpenClaw 子进程生命周期 + WebSocket JSON-RPC 连接 ▼ Gateway Process (OpenClaw) │ 读取 openclaw.json 的 provider 配置 │ 负责真正的 LLM HTTP 请求、流式响应、工具调用、会话历史 ▼ 调用实际 LLM API -> 流式返回 chunk -> GatewayManager -> Renderer ``` **zn-ai 当前架构:** ``` Renderer Chat Store │ WebSocketManager.sendMessage() -> 云端后端 ▼ zn-ai Cloud Backend (wss://onefeel.brother7.cn) │ 固定使用阿里百炼千问 ▼ 返回响应 ``` **关键差异:** - ClawX 有**本地 Gateway 子进程**,zn-ai 没有; - ClawX Chat Store **只与 Gateway 通信**(`gateway:rpc`),zn-ai Chat Store 直接与云端 WebSocket 通信; - ClawX 的模型切换/调用完全由 Gateway 内部决定,zn-ai 由远端后端硬编码决定。 --- ## 二、重构总体目标 建立 zn-ai 的**轻量级本地 Gateway Service**,完全替代现有的云端 WebSocket 对话后端: 1. **Gateway 作为唯一对话入口**:Renderer Chat Store 仅通过 `gateway:rpc` IPC 与 Gateway 通信; 2. **本地 Provider 直连 LLM**:Gateway 读取 `provider-api-service` 的配置,直接调用 OpenAI/Anthropic 等 LLM API; 3. **流式响应本地处理**:Gateway 负责流式接收 chunk,并通过事件通道推回 Renderer; 4. **会话历史本地管理**:Gateway 维护会话历史,支持 `chat.history` 查询; 5. **Provider 变更自动 reload**:修改 provider 配置后,Gateway 自动热重载配置; 6. **未来可扩展 Tool Calling/Agent Loop**:所有复杂逻辑集中在 Gateway,Renderer 保持简单。 --- ## 三、目标架构设计 ### 3.1 整体数据流 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Renderer 进程 │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ ChatBox.vue │◄──►│ chatStore │ │ │ └──────────────┘ └──────┬───────┘ │ │ │ │ │ invokeIpc('gateway:rpc', ...) │ └─────────────────────────────┼───────────────────────────────────────────────┘ │ IPC ┌─────────────────────────────┼───────────────────────────────────────────────┐ │ Electron Main │ │ ┌──────────────────────────┴────────────────────────────────────┐ │ │ │ GatewayManager (新建) │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ │ │ Gateway RPC │ │ Provider │ │ Session Store │ │ │ │ │ │ Server │ │ Resolver │ │ (内存/本地持久化) │ │ │ │ │ └──────┬──────┘ └──────┬──────┘ └─────────────────────┘ │ │ │ │ │ │ │ │ │ │ └────────────────┘ │ │ │ │ │ │ │ │ │ ┌──────────▼──────────┐ │ │ │ │ │ ChatHandler │ │ │ │ │ │ (chat.send/history)│ │ │ │ │ └──────────┬──────────┘ │ │ │ │ │ │ │ │ │ ┌──────────▼──────────┐ │ │ │ │ │ Provider Factory │ │ │ │ │ │ (createProvider) │ │ │ │ │ └──────────┬──────────┘ │ │ │ │ │ │ │ │ │ ┌───────────────┼───────────────┐ │ │ │ │ ▼ ▼ ▼ │ │ │ │ OpenAIProvider AnthropicProvider OllamaProvider │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ ▼ ▼ ▼ │ │ provider-accounts.json provider-keys.json (可选) IndexedDB │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ ``` ### 3.2 Gateway 接口契约(对齐 ClawX) Renderer 通过 `gateway:rpc` IPC 调用 Gateway 方法: ```ts // IPC 调用方式 window.api.invoke('gateway:rpc', method: string, params: any) // 示例 invokeIpc('gateway:rpc', 'chat.send', { sessionKey: 'agent:deepseek:xxx', message: { role: 'user', content: '你好' }, options: { providerAccountId: 'deepseek-xxx' } }) invokeIpc('gateway:rpc', 'chat.history', { sessionKey: 'agent:deepseek:xxx', limit: 50, }) invokeIpc('gateway:rpc', 'chat.abort', { sessionKey: 'agent:deepseek:xxx', }) ``` Gateway 向 Renderer 推送事件(通过 `ipcMain` -> `mainWindow.webContents.send`): ```ts // 事件名称对齐 ClawX type GatewayEvent = | { type: 'chat:delta'; sessionKey: string; runId: string; delta: string } | { type: 'chat:final'; sessionKey: string; runId: string; message: ChatMessage } | { type: 'chat:error'; sessionKey: string; runId: string; error: string } | { type: 'chat:aborted'; sessionKey: string; runId: string } | { type: 'gateway:status'; status: 'connected' | 'disconnected' | 'reconnecting' } ``` --- ## 四、迁移开发规划 ### Phase 1: 轻量级 Gateway Service 骨架(基础设施) #### 4.1.1 新建 `electron/gateway/` 目录 文件清单: - `electron/gateway/manager.ts` — GatewayManager 主类 - `electron/gateway/types.ts` — Gateway RPC 类型定义 - `electron/gateway/handlers/chat.ts` — Chat RPC 处理器 - `electron/gateway/handlers/provider.ts` — Provider 查询 RPC 处理器 - `electron/gateway/session-store.ts` — 会话历史内存存储 #### 4.1.2 `GatewayManager` 核心职责 ```ts class GatewayManager { // 初始化时加载 provider 配置 async init(): Promise // RPC 入口,被 ipcMain.handle('gateway:rpc') 调用 async rpc(method: string, params: any): Promise // 向所有 renderer 窗口广播事件 broadcast(event: GatewayEvent): void // 监听 provider 配置变化并热重载 reloadProviders(): void } ``` **设计决策**:zn-ai 不需要像 ClawX 那样启动独立的 OpenClaw 子进程。Gateway 直接以**Electron Main 进程内的 Service 类**形式存在,避免进程间通信的复杂度,同时保持接口与 ClawX 完全一致。 #### 4.1.3 注册 `gateway:rpc` IPC Handler 在 `electron/main.ts` 中新增: ```ts import { gatewayManager } from '@electron/gateway/manager'; app.whenReady().then(() => { gatewayManager.init(); // ... existing code }); ipcMain.handle('gateway:rpc', async (_event, method: string, params: any) => { return gatewayManager.rpc(method, params); }); // 转发 Gateway 事件到 Renderer ipcMain.on('gateway:subscribe', (event) => { // 可选:若需要按需订阅 }); ``` --- ### Phase 2: Provider 层改造(能力层) #### 4.2.1 重构 `electron/providers/index.ts` 废弃旧的 `CONFIG_KEYS.PROVIDER` base64 解析逻辑,改为从 `providerApiService` 读取: ```ts import { providerApiService } from '@electron/service/provider-api-service'; export function createProvider(accountId: string): BaseProvider { const account = providerApiService.getAccounts().find(a => a.id === accountId); if (!account) throw new Error(`Provider account ${accountId} not found`); const apiKey = providerApiService.getApiKey(accountId).apiKey; if (!apiKey) throw new Error(`API key for account ${accountId} not found`); const baseURL = account.baseUrl || getProviderTypeInfo(account.vendorId)?.defaultBaseUrl; if (!baseURL) throw new Error(`Base URL for account ${accountId} not found`); switch (account.apiProtocol) { case 'anthropic-messages': // return new AnthropicProvider(apiKey, baseURL, account.headers); throw new Error('Anthropic provider not yet implemented'); case 'openai-completions': case 'openai-responses': default: return new OpenAIProvider(apiKey, baseURL, account.headers); } } ``` #### 4.2.2 扩展 `OpenAIProvider` - 构造函数增加 `headers?: Record` 参数; - `chat()` 方法增加 `options?: { signal?: AbortSignal }` 支持; - 在 `client` 初始化时透传自定义 headers(如 Ark 的额外认证头)。 #### 4.2.3 `BaseProvider` 接口统一 ```ts export abstract class BaseProvider { abstract chat( messages: ChatMessage[], model: string, options?: { signal?: AbortSignal } ): Promise>; } ``` --- ### Phase 3: Gateway Chat 处理器实现(核心链路) #### 4.3.1 `electron/gateway/handlers/chat.ts` 实现三个核心 RPC 方法: **A. `chat.send`** ```ts async function handleChatSend(params: { sessionKey: string; message: ChatMessage; options?: { providerAccountId?: string }; }): Promise<{ runId: string }> { const runId = crypto.randomUUID(); const session = sessionStore.getOrCreate(params.sessionKey); // 1. 追加用户消息到会话历史 session.appendMessage(params.message); // 2. 解析 Provider const accountId = params.options?.providerAccountId || providerApiService.getDefault().accountId; if (!accountId) throw new Error('No provider account selected'); const provider = createProvider(accountId); const account = providerApiService.getAccounts().find(a => a.id === accountId)!; // 3. 构建 messages 数组(历史 + 当前消息) const messages = buildChatMessages(session.messages, account); // 4. 启动流式调用 const abortController = new AbortController(); session.setActiveRun(runId, abortController); // 5. 异步流式处理 processChatStream(session, runId, provider, account.model!, messages, abortController.signal); return { runId }; } ``` **B. `chat.history`** ```ts function handleChatHistory(params: { sessionKey: string; limit?: number }): ChatMessage[] { const session = sessionStore.get(params.sessionKey); return session?.getMessages(params.limit) ?? []; } ``` **C. `chat.abort`** ```ts function handleChatAbort(params: { sessionKey: string }): void { const session = sessionStore.get(params.sessionKey); if (session?.activeRun) { session.activeRun.abortController.abort(); session.clearActiveRun(); gatewayManager.broadcast({ type: 'chat:aborted', sessionKey: params.sessionKey, runId: session.activeRun.runId, }); } } ``` #### 4.3.2 流式处理逻辑 `processChatStream` ```ts async function processChatStream( session: Session, runId: string, provider: BaseProvider, model: string, messages: ChatMessage[], signal: AbortSignal ) { let assistantContent = ''; try { const chunks = await provider.chat(messages, model, { signal }); for await (const chunk of chunks) { if (signal.aborted) break; if (chunk.result) { assistantContent += chunk.result; gatewayManager.broadcast({ type: 'chat:delta', sessionKey: session.key, runId, delta: chunk.result, }); } if (chunk.isEnd) { break; } } if (!signal.aborted) { const finalMessage: ChatMessage = { role: 'assistant', content: assistantContent, timestamp: Date.now(), }; session.appendMessage(finalMessage); session.clearActiveRun(); gatewayManager.broadcast({ type: 'chat:final', sessionKey: session.key, runId, message: finalMessage, }); } } catch (error) { session.clearActiveRun(); gatewayManager.broadcast({ type: 'chat:error', sessionKey: session.key, runId, error: error instanceof Error ? error.message : String(error), }); } } ``` --- ### Phase 4: Renderer Chat Store 重构(UI 层) #### 4.4.1 新增 `src/lib/gateway-client.ts` 封装 `gateway:rpc` 调用,与 ClawX 的 `api-client.ts` 对齐: ```ts export async function gatewayRpc(method: string, params?: any): Promise { if (!window.api?.invoke) { throw new Error('IPC not available'); } return window.api.invoke('gateway:rpc', method, params); } export function onGatewayEvent( callback: (event: GatewayEvent) => void ): () => void { const handler = (_event: any, payload: GatewayEvent) => callback(payload); window.api.on('gateway:event', handler); return () => window.api.off('gateway:event', handler); } ``` **注意**:需要在 `electron/preload/index.ts` 中暴露对应的 IPC 通道。 #### 4.4.2 重构 `src/store/chat.ts` **废弃内容:** - `WebSocketManager` 及其所有生命周期管理(`initConnection`, `_wsManager`, `_adaptWsMessage`); - 云端 WebSocket URL 和消息格式; - 基于 HTTP API 的 `getSessionMessages` 历史加载(逐步替换为 `gateway:rpc chat.history`)。 **新增/改造内容:** **A. 状态扩展** ```ts interface ChatState { // ... 现有状态保留 gatewayStatus: 'connected' | 'disconnected' | 'reconnecting'; } ``` **B. `sendMessage` 重构** ```ts async sendMessage(text, attachments) { const trimmed = text.trim(); if (!trimmed && (!attachments || attachments.length === 0)) return; const providerStore = useProviderStore(); const defaultAccountId = providerStore.defaultAccountId; if (!defaultAccountId) { this.error = '请先前往模型管理页面配置并设置一个默认模型'; return; } // 确保有有效 sessionKey const sessionKey = this.currentSessionKey; if (!sessionKey || sessionKey === DEFAULT_SESSION_KEY) { // 本地会话:使用本地生成的 key const localKey = `local:${defaultAccountId}:${crypto.randomUUID()}`; this.currentSessionKey = localKey; this.sessions = ensureSessionEntry(this.sessions, { key: localKey, displayName: 'New Chat' }); } // 乐观添加用户消息 const userMsg = buildUserMessage(trimmed, attachments); this.messages = [...this.messages, userMsg]; this.sending = true; this.error = null; this.streamingText = ''; this.streamingMessage = null; // 通过 Gateway 发送(复用 Gateway 返回的 runId) try { const { runId } = await gatewayRpc<{ runId: string }>('chat.send', { sessionKey: this.currentSessionKey, message: { role: 'user', content: trimmed }, options: { providerAccountId: defaultAccountId, }, }); this.activeRunId = runId; } catch (err) { this.error = String(err); this.sending = false; this.activeRunId = null; } } ``` **C. `handleChatEvent` 适配 Gateway 事件** 现有 `handleChatEvent` 的逻辑基本无需改动,只需调整事件来源: - 在 chat store 初始化时订阅 `gateway:event`; - 将 Gateway 推送的 `chat:delta` / `chat:final` / `chat:error` / `chat:aborted` 映射到现有状态机。 **D. `loadHistory` 重构** ```ts async loadHistory(quiet = false) { const { currentSessionKey } = this; if (!currentSessionKey || currentSessionKey === DEFAULT_SESSION_KEY) { this.messages = []; return; } // 本地会话走 Gateway if (currentSessionKey.startsWith('local:')) { if (!quiet) this.loading = true; try { const messages = await gatewayRpc('chat.history', { sessionKey: currentSessionKey, limit: 50, }); this.messages = messages; } catch (err) { console.warn('Failed to load local history:', err); } finally { this.loading = false; } return; } // 遗留的云端会话暂时保留原有 HTTP API 加载逻辑(用于兼容) // ... 现有 getSessionMessages 逻辑 } ``` **E. `abortRun` 重构** ```ts async abortRun() { clearHistoryPoll(); this.sending = false; this.streamingText = ''; this.streamingMessage = null; this.activeRunId = null; try { await gatewayRpc('chat.abort', { sessionKey: this.currentSessionKey, }); } catch (err) { console.warn('Abort failed:', err); } } ``` #### 4.4.3 Preload 脚本扩展 在 `electron/preload/index.ts` 中确保暴露: ```ts contextBridge.exposeInMainWorld('api', { invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args), on: (channel: string, callback: (...args: any[]) => void) => ipcRenderer.on(channel, callback), off: (channel: string, callback: (...args: any[]) => void) => ipcRenderer.off(channel, callback), // ... 现有方法 }); ``` --- ### Phase 5: Provider Store 与默认模型(UI 层) #### 4.5.1 精简 `src/store/providers.ts` - 移除 `selectedAccountId` 状态及相关 `localStorage` 持久化; - 对话层不再维护独立的模型选中态,直接复用 `defaultAccountId`。 #### 4.5.2 Chat 页面移除模型选择器 - 从 `ChatBox.vue` 中移除 `ModelSelector` 组件及其引用; - 对话功能直接使用 provider 管理中「设置为默认」的模型账户; - 对话页面 `onMounted` 时主动调用 `providerStore.init()` 加载模型配置。 #### 4.5.3 自动默认模型兜底 当 `providerStore.init()` 初始化后发现用户已存在 provider accounts 但未设置 `defaultAccountId` 时,自动将第一个 account 设为默认,确保用户无需手动点击"设为默认"即可直接开始对话。若用户没有任何账户,则保持原有提示行为。 --- ### Phase 6: 会话历史持久化(数据层) #### 4.6.1 会话历史存储策略 当前 `session-store.ts` 使用内存存储。为支持应用重启后恢复,引入本地持久化: **方案 A(推荐,MVP 阶段)**: - Gateway 的 `sessionStore` 在进程退出时将数据写入 `userData/chat-sessions.json`; - 启动时从该文件加载; - 数据结构:`Record`。 **方案 B(进阶)**: - 使用 `electron-store` 或 IndexedDB 存储; - 支持按会话搜索、导出、删除。 #### 4.6.2 会话标识区分 - **本地 Gateway 会话**:`local:{providerAccountId}:{uuid}`; - **云端遗留会话**:保持原有的 `agent:{agentId}:main` 或后端分配的 `session_id`。 #### 4.6.3 会话列表混合显示 - `loadSessions()` 中: - 云端会话继续调用 `getSessionList`; - 本地会话从 `sessionStore` 读取; - 合并后按 `updatedAt` 排序显示。 - 短期可先只显示本地会话(如果决定完全废弃云端)。 --- ### Phase 7: Provider 配置变更与 Gateway 热重载 #### 4.7.1 Provider 变更通知机制 在 `electron/service/provider-api-service/index.ts` 中引入订阅模式: ```ts type ProviderChangeListener = () => void; const listeners: ProviderChangeListener[] = []; export function onProviderChange(listener: ProviderChangeListener): () => void { listeners.push(listener); return () => { const idx = listeners.indexOf(listener); if (idx > -1) listeners.splice(idx, 1); }; } function notifyChange() { listeners.forEach(l => l()); } ``` 在 `createAccount`, `updateAccount`, `deleteAccount`, `setDefault` 等写操作后调用 `notifyChange()`。 #### 4.7.2 Gateway 监听并重载 在 `GatewayManager.init()` 中: ```ts onProviderChange(() => { this.reloadProviders(); }); ``` `reloadProviders()` 实现: - 重新读取所有 provider 账户和 API keys; - 对于**没有活跃 run** 的 provider,立即替换内存中的配置; - 对于有活跃 run 的 provider,标记为「待更新」,在 run 结束后应用新配置。 --- ### Phase 8: 多 Provider 适配矩阵(能力扩展) | Vendor | 实现类 | 协议 | 优先级 | |--------|--------|------|--------| | OpenAI | `OpenAIProvider` | `openai` SDK | P0 | | DeepSeek | `OpenAIProvider` | `openai` SDK | P0 | | Moonshot | `OpenAIProvider` | `openai` SDK | P0 | | SiliconFlow | `OpenAIProvider` | `openai` SDK | P0 | | Ark (豆包) | `OpenAIProvider` | `openai` SDK + headers | P0 | | 百度千帆 | `OpenAIProvider` | `openai` SDK | P1 | | Ollama | `OpenAIProvider` | `openai` SDK | P1 | | Anthropic | `AnthropicProvider` | `@anthropic-ai/sdk` | P2 | | Google Gemini | `GeminiProvider` | `@google/generative-ai` | P2 | 所有 OpenAI-compatible 的厂商优先通过 `OpenAIProvider` + `baseURL` + `headers` 支持。 --- ## 五、实施优先级与里程碑 ### Milestone 1: Gateway 骨架 + 本地 Provider 可对话(2 周) - [ ] Phase 1: 创建 `electron/gateway/` 目录与 `GatewayManager` 骨架 - [ ] Phase 2: 重构 `createProvider` 读取 `provider-api-service` - [ ] Phase 3: 实现 `chat.send` / `chat.history` / `chat.abort` RPC 处理器 - [ ] Phase 4: 重构 `chatStore.sendMessage` 走 `gateway:rpc` - [ ] Phase 5: 对话页面移除模型选择器,直接使用默认模型 - [ ] 验证:添加 DeepSeek 并设为默认 -> 对话返回 DeepSeek 模型内容 ### Milestone 2: 历史持久化 + 体验优化(1 周) - [ ] Phase 6: 实现 `chat-sessions.json` 本地持久化 - [ ] 支持会话列表混合显示(本地 + 云端遗留) - [ ] 支持图片附件输入、Abort、重发 - [ ] API Key 失效、网络超时等错误友好提示 ### Milestone 3: 完全废弃云端 WebSocket + 架构收尾(1-2 周) - [ ] Phase 7: Provider 变更自动通知 Gateway 重载 - [ ] 彻底移除 `WebSocketManager` 及相关代码 - [ ] 废弃 `START_A_DIALOGUE` IPC(被 `gateway:rpc` 完全替代) - [ ] 清理云端后端相关的历史兼容代码 - [ ] 引入 Anthropic/Gemini Provider 支持(可选) --- ## 五之一、Sub Agent 分工与数量估算 基于本次更新后的迁移规划,建议分配 **6 个 Sub Agent** 并行作业: ### 对话功能核心检查(4 个 Agent) | Sub Agent | 负责领域 | 对应 Phase | 核心任务 | |-----------|----------|------------|----------| | **Agent 1** | Gateway 主进程 | Phase 1, 3, 6, 7 | 检查并修复 `GatewayManager` 骨架;验证 `chat.send` / `chat.history` / `chat.abort` 实现;会话持久化到 `chat-sessions.json`;Provider 变更热重载 | | **Agent 2** | Provider 层 | Phase 2, 8 | 检查并修复 `createProvider` 本地配置读取;验证 `OpenAIProvider`(headers、AbortSignal、错误处理);扩展 DeepSeek 等内置厂商 | | **Agent 3** | Renderer Chat Store | Phase 4 | 检查并修复 `src/store/chat.ts`:废弃 WebSocket,接入 `gateway:rpc`;验证事件映射 `chat:delta/final/error/aborted`;维护流式状态机 | | **Agent 4** | UI 与交互 | Phase 5 | 检查并修复 `ChatBox.vue` 中 `ModelSelector` 移除情况;验证对话直接使用 `defaultAccountId`;空状态、错误提示、附件输入等 UI 细节 | ### 集成与模型管理(2 个 Agent) | Sub Agent | 负责领域 | 核心任务 | |-----------|----------|----------| | **Agent 5** | 对话功能集成 | 运行端到端类型检查与编译;验证 `gateway:rpc` → Provider → Renderer 的完整链路;确认流式响应、会话历史、Abort 等功能闭环 | | **Agent 6** | 模型管理功能 | 检查并修复「设置为默认模型」功能;验证 `defaultAccountId` 在 Provider 管理页的正确设置与持久化;确保对话层能正确读取默认模型 | **估算依据:** - 前 4 个 Agent 分别对应「主进程网关 → Provider 能力 → 渲染层状态 → 前端 UI」四层架构,边界清晰、耦合最小。 - Agent 5 作为独立集成 Agent,专门负责跨层契约验证(`gateway:rpc` / `gateway:event` / `createProvider()`)和端到端测试,不与其他 4 个 Agent 的职责重叠。 - Agent 6 单独负责模型管理模块的「设置默认模型」功能,这是对话层能正确调用默认 Provider 的前置依赖。 - 6 个 Agent 并行可最大化效率:Agent 1-4 检查各自层的实现,Agent 5 等待 1-4 完成后做集成验证,Agent 6 可与 1-4 并行检查模型管理页面。 ## 六、风险与应对 | 风险 | 影响 | 应对策略 | |------|------|----------| | **云端后端有其他业务逻辑**(如 Agent 工具链、RAG) | 完全废弃云端后这些能力丢失 | 短期保留云端会话的只读访问;长期将这些能力迁移到本地 Gateway 中实现 | | **图片/文件输入格式差异** | 不同厂商 vision API 格式不同 | Gateway 内部统一转换为 OpenAI vision 格式再发送 | | **会话历史分裂** | 本地和云端会话无法统一管理 | 本地会话使用 `local:` 前缀区分;会话列表合并显示 | | **AbortController 兼容性** | 旧 Electron 版本可能不支持 | Electron 40 基于 Chromium 124,完全支持 `AbortController` | | **Gateway 阻塞主进程** | 大量并发流式响应可能阻塞 Electron Main | Gateway 使用异步迭代器,不会阻塞事件循环;必要时将来可拆分为 Worker 线程 | --- ## 七、关键文件变更清单 ### 新建文件 - `electron/gateway/manager.ts` - `electron/gateway/types.ts` - `electron/gateway/handlers/chat.ts` - `electron/gateway/handlers/provider.ts` - `electron/gateway/session-store.ts` - `src/lib/gateway-client.ts` ### 主进程修改 - `electron/main.ts` — 注册 `gateway:rpc` IPC,初始化 `GatewayManager` - `electron/providers/index.ts` — 废弃旧配置读取,改为从 `providerApiService` 读取 - `electron/providers/OpenAIProvider.ts` — 扩展 headers、AbortSignal 支持 - `electron/service/provider-api-service/index.ts` — 增加变更通知机制 - `electron/wins/index.ts` — 废弃 `START_A_DIALOGUE` IPC Handler - `electron/preload/index.ts` — 确保暴露 `gateway:event` 通道 ### 渲染进程修改 - `src/store/chat.ts` — 全面重构:废弃 WebSocket,接入 `gateway:rpc` - `src/store/providers.ts` — 移除 `selectedAccountId`,对话直接使用 `defaultAccountId` - `src/pages/home/ChatBox.vue` — 移除模型选择器,简化输入区 ### 待废弃文件 - `src/lib/WebSocketManager.ts`(Milestone 3 移除) - `src/api/SessionsApi.ts` 中的部分云端接口(Milestone 3 评估后移除) --- ## 八、总结 **当前问题本质**:zn-ai 的对话系统绕过了本地模型管理,直接连接了一个使用固定模型的云端后端。 **对齐 ClawX 的核心路径**: 1. 在 Electron Main 进程内建立**轻量级 Gateway Service**; 2. 让 Gateway 成为 Renderer Chat Store 的**唯一对话入口**(`gateway:rpc`); 3. Gateway 内部读取 `provider-api-service` 配置,**直接调用 LLM API**; 4. 流式响应通过 Gateway 事件通道推回 Renderer; 5. 对话页面**移除模型选择器**,直接使用 Provider 管理中「设置为默认」的模型; 6. 逐步废弃 WebSocket 云端后端和 `START_A_DIALOGUE` 旧 IPC。 **最终收益**: - Renderer 代码大幅简化,与 ClawX 100% 对齐; - 模型选择、LLM 调用、流式处理、会话历史全部本地化; - 未来接入 Tool Calling、Agent Loop 可直接在 Gateway 中实现,无需改动 UI。