feat: 重构对话功能
This commit is contained in:
747
docs/model-chat-migration-plan.md
Normal file
747
docs/model-chat-migration-plan.md
Normal 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**:所有复杂逻辑集中在 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<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。
|
||||
Reference in New Issue
Block a user