feat: 重构对话功能

This commit is contained in:
DEV_DSW
2026-04-14 17:02:20 +08:00
parent b3f07c4cfe
commit c61e41049f
53 changed files with 5200 additions and 1982 deletions

View File

@@ -0,0 +1,747 @@
# 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**:所有复杂逻辑集中在 GatewayRenderer 保持简单。
---
## 三、目标架构设计
### 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<void>
// RPC 入口,被 ipcMain.handle('gateway:rpc') 调用
async rpc(method: string, params: any): Promise<any>
// 向所有 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<string, string>` 参数;
- `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<AsyncIterable<UniversalChunk>>;
}
```
---
### 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<T>(method: string, params?: any): Promise<T> {
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<RawMessage[]>('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 管理中「设置为默认」的模型账户;
- 若未设置默认模型,提示用户前往模型管理页面配置。
---
### Phase 6: 会话历史持久化(数据层)
#### 4.6.1 会话历史存储策略
当前 `session-store.ts` 使用内存存储。为支持应用重启后恢复,引入本地持久化:
**方案 A推荐MVP 阶段)**
- Gateway 的 `sessionStore` 在进程退出时将数据写入 `userData/chat-sessions.json`
- 启动时从该文件加载;
- 数据结构:`Record<sessionKey, { messages: ChatMessage[], updatedAt: number }>`
**方案 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。