30 KiB
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 对话后端:
- Gateway 作为唯一对话入口:Renderer Chat Store 仅通过
gateway:rpcIPC 与 Gateway 通信; - 本地 Provider 直连 LLM:Gateway 读取
provider-api-service的配置,直接调用 OpenAI/Anthropic 等 LLM API; - 流式响应本地处理:Gateway 负责流式接收 chunk,并通过事件通道推回 Renderer;
- 会话历史本地管理:Gateway 维护会话历史,支持
chat.history查询; - Provider 变更自动 reload:修改 provider 配置后,Gateway 自动热重载配置;
- 未来可扩展 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 方法:
// 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):
// 事件名称对齐 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 核心职责
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 中新增:
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 读取:
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 接口统一
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
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
function handleChatHistory(params: { sessionKey: string; limit?: number }): ChatMessage[] {
const session = sessionStore.get(params.sessionKey);
return session?.getMessages(params.limit) ?? [];
}
C. chat.abort
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
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 对齐:
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. 状态扩展
interface ChatState {
// ... 现有状态保留
gatewayStatus: 'connected' | 'disconnected' | 'reconnecting';
}
B. sendMessage 重构
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 重构
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 重构
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 中确保暴露:
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 中引入订阅模式:
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() 中:
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.abortRPC 处理器 - 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_DIALOGUEIPC(被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.tselectron/gateway/types.tselectron/gateway/handlers/chat.tselectron/gateway/handlers/provider.tselectron/gateway/session-store.tssrc/lib/gateway-client.ts
主进程修改
electron/main.ts— 注册gateway:rpcIPC,初始化GatewayManagerelectron/providers/index.ts— 废弃旧配置读取,改为从providerApiService读取electron/providers/OpenAIProvider.ts— 扩展 headers、AbortSignal 支持electron/service/provider-api-service/index.ts— 增加变更通知机制electron/wins/index.ts— 废弃START_A_DIALOGUEIPC Handlerelectron/preload/index.ts— 确保暴露gateway:event通道
渲染进程修改
src/store/chat.ts— 全面重构:废弃 WebSocket,接入gateway:rpcsrc/store/providers.ts— 移除selectedAccountId,对话直接使用defaultAccountIdsrc/pages/home/ChatBox.vue— 移除模型选择器,简化输入区
待废弃文件
src/lib/WebSocketManager.ts(Milestone 3 移除)src/api/SessionsApi.ts中的部分云端接口(Milestone 3 评估后移除)
八、总结
当前问题本质:zn-ai 的对话系统绕过了本地模型管理,直接连接了一个使用固定模型的云端后端。
对齐 ClawX 的核心路径:
- 在 Electron Main 进程内建立轻量级 Gateway Service;
- 让 Gateway 成为 Renderer Chat Store 的唯一对话入口(
gateway:rpc); - Gateway 内部读取
provider-api-service配置,直接调用 LLM API; - 流式响应通过 Gateway 事件通道推回 Renderer;
- 对话页面移除模型选择器,直接使用 Provider 管理中「设置为默认」的模型;
- 逐步废弃 WebSocket 云端后端和
START_A_DIALOGUE旧 IPC。
最终收益:
- Renderer 代码大幅简化,与 ClawX 100% 对齐;
- 模型选择、LLM 调用、流式处理、会话历史全部本地化;
- 未来接入 Tool Calling、Agent Loop 可直接在 Gateway 中实现,无需改动 UI。