Files
zn-ai/docs/model-chat-migration-plan.md
2026-04-14 17:02:20 +08:00

30 KiB
Raw Blame History

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:rpczn-ai Chat Store 直接与云端 WebSocket 通信;
  • ClawX 的模型切换/调用完全由 Gateway 内部决定zn-ai 由远端后端硬编码决定。

二、重构总体目标

建立 zn-ai 的轻量级本地 Gateway Service,完全替代现有的云端 WebSocket 对话后端:

  1. Gateway 作为唯一对话入口Renderer Chat Store 仅通过 gateway:rpc IPC 与 Gateway 通信;
  2. 本地 Provider 直连 LLMGateway 读取 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 方法:

// 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.abort RPC 处理器
  • Phase 4: 重构 chatStore.sendMessagegateway: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 IPCgateway: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.jsonProvider 变更热重载
Agent 2 Provider 层 Phase 2, 8 检查并修复 createProvider 本地配置读取;验证 OpenAIProviderheaders、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.vueModelSelector 移除情况;验证对话直接使用 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.tsMilestone 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。