Files
zn-ai/docs/model-chat-migration-plan.md
DEV_DSW 416399e7a8 feat: implement menu service for context menu management
feat: add provider API service for managing provider accounts and keys
feat: create provider runtime sync service for agent runtime management
feat: introduce script execution service for running automation scripts
feat: develop script store service for managing script metadata and storage
feat: implement theme service for managing application theme settings
feat: add updater service for handling application updates
feat: create window service for managing application windows and their states
2026-04-22 09:26:39 +08:00

751 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 管理中「设置为默认」的模型账户;
- 对话页面 `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<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.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.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。