Ara / 博客 / Rust WebSocket 代理与三层解耦

Rust WebSocket 代理:Ara 的三层解耦架构

2026-07-30 · #Tauri #Rust #IPC #工程实践

v0.2 是 Ara Desktop 的第一个架构里程碑。在需求明确(通用 Agent 桌面客户端、使用 Tauri v2)之后,第一个要解决的问题不是 UI 布局或功能细节,而是决定"后端通信的线程"。

这篇文章记录从"前端直连 WebSocket"到"Rust IPC 代理"的架构转变,以及在这个过程中发现的 Tauri 工程实践。

初始架构的问题

Ara Desktop 的日常使用场景是:桌面应用(Tauri)→ WebSocket → Python Hermes Agent。v0.1 的原型采用了最直接的路径——前端 React 直接 new WebSocket() 连 Python 后端:

┌─────────────────┐ WS (127.0.0.1:9119) ┌────────────────────┐ │ React (Tauri) │ ───────────────────────────────────► │ Python Hermes │ │ useHermesWS() │ ◄─────────────────────────────────── │ │ └─────────────────┘ stream └────────────────────┘

这个模式在技术验证阶段完全合理——最小路径,快速验证通信链路。但如果一直这么用下去,Tauri 就只是一个 WebView 容器,失去了桌面端框架的核心价值。

具体痛点:

三层解耦架构

v0.2 重构的核心思路:所有后端通信经过 Rust IPC 层。前端只通过 invoke() 收发消息,不感知后端的具体协议。

┌─────────────────┐ Tauri IPC ┌──────────────────────┐ ws://host:port/ws ┌────────────────────┐ │ React │ ◄── events ─── │ Rust (BackendManager)│ ──────────────────────► │ Python Hermes │ │ ChatView │ ── invoke() ──► │ tokio-tungstenite │ ◄────────────────────── │ │ │ StatusBar │ │ 连接/消息/状态管理 │ stream └────────────────────┘ └─────────────────┘ └──────────────────────┘

关键变更:

BackendManager 设计

Rust 层的核心是 BackendManager,它负责:

前端通过 4 个 Tauri 命令与 BackendManager 交互:

// Rust 侧 (commands.rs)
#[tauri::command]
async fn connect_backend(app, state, host, port) → Result
#[tauri::command]
async fn disconnect_backend(state) → Result
#[tauri::command]
async fn send_message(state, content) → Result
#[tauri::command]
async fn get_backend_status(state) → Result<BackendStatus>

// 前端调用
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";

await invoke("connect_backend", { host: "127.0.0.1", port: 9119 });

const unlisten = await listen("backend-event", (event) => {
  // event.payload.type ∈ ["status_change","message","stream_start","stream_chunk","stream_end","error"]
});

流式消息透传的实现

流式消息是 Agent 交互的核心体验。在 Rust 层做代理意味着需要把 ws_loop 中的接收循环与 Tauri 的事件系统桥接起来。

关键实现细节:

// ws_loop 的核心循环 (tokio::select! 多路复用)
loop {
    tokio::select! {
        // 从前端的 mpsc channel 接收消息,转发给 WS
        Some(msg) = rx.recv() => {
            ws_write.send(Message::Text(msg)).await?
        }
        // 从 WS 接收消息,解析后 emit 给前端
        Some(ws_msg) = ws_read.next() => {
            match ws_msg {
                Ok(Message::Text(text)) => {
                    handle_ws_message(&app, &text).await;
                }
                Ok(Message::Close(_)) => {
                    // 更新状态 + 通知前端
                    break;
                }
                Err(e) => {
                    // 错误处理
                    break;
                }
                _ => {}
            }
        }
    }
}

每条 WS 消息在 handle_ws_message 中解析为 JSON,根据 type 字段分发为不同的 BackendEvent 变体,通过 app.emit() 发送到前端。前端在 React 组件中通过 listen("backend-event", callback) 接收并更新 UI 状态。

协议抽象的前瞻

虽然 v0.2 只实现了 Hermes WebSocket 协议,但架构为后续的协议抽象预留了空间。v0.4 的目标是实现:

// 未来协议抽象 trait
trait AgentBackend {
    async fn connect(&self, config: BackendConfig) -> Result<()>;
    async fn send_message(&self, content: &str) -> Result<()>;
    fn stream_events(&self) -> BoxStream<AgentEvent>;
    async fn disconnect(&self) -> Result<()>;
}

届时 v0.2 的 BackendManager 将成为 HermesWSAdapter 的实现,新增后端只需要实现这个 trait。前端无需感知切换。

前端配套改造

架构变更不只影响 Rust 层,前端也做了对应的重构:

构建验证

重构完成后,完整构建通过:

pnpm tauri build --bundles dmg

产物:
  Ara Desktop.app        → 11 MB
  Ara Desktop_0.1.0_aarch64.dmg → 3.6 MB

验证:
  pnpm tsc --noEmit      → 零错误
  cargo check            → 零 warnings
  vite build             → 23 modules, 199KB JS + 1.5KB CSS

Tauri 的 .app 保持了 11MB 的典型大小,新增的 tokio-tungstenite 依赖没有让包体膨胀。


这次重构的核心理念是:在做功能之前先做对架构。三层解耦让 Ara 的每个层次只关注自己的职责——前端只做 UI,Rust 层管通信和后端生命周期,Python 层管 Agent 逻辑。

下一阶段(v0.3)将在这个架构上构建双面板布局和嵌入式终端。