新增: 跨端WS隧道脚手架(df-tunnel客户端+df-relay云中继)

df-tunnel:桌面端出站WS客户端(TunnelClient trait+骨架,穿NAT连云后端)
df-relay:axum WS中继服务(转发小程序↔桌面端,广播骨架)
双crate独立消息骨架,不依赖src-tauri/df-types避跨crate强耦合
workspace crates/* glob自动注册,Phase2填充WS握手/鉴权/重连/路由
This commit is contained in:
2026-06-22 01:24:01 +08:00
parent 566bbcb394
commit 5d1749a58f
11 changed files with 818 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
//! df-tunnel 隧道客户端
//!
//! 设计依据:设计文档「Layer1」—— 桌面端主动出站连云后端 `wss://your-server/ws/device`,
//! 穿 NAT 无需端口映射。本模块定义 `TunnelClient` trait + tokio-tungstenite WS 连接骨架。
//!
//! 脚手架范围:trait 定义 + 连接骨架(占位实现),Phase2 填:
//! - 实际 WS 握手 + 鉴权(token / device_id)
//! - 断线指数退避重连
//! - 事件桥接(app.emit 同步 clone 推隧道)
//! - 指令路由(收到 TunnelCommand 调 Tauri command)
use async_trait::async_trait;
use crate::error::Result;
use crate::events::{TunnelCommand, TunnelEvent};
/// 隧道客户端抽象
///
/// 设计为 trait 而非具体结构,便于:
/// 1. 测试用 mock 实现(不依赖真实 WS)
/// 2. 未来替换底层实现(如换 sse / quic)不改调用方
///
/// 所有方法 async + 返回 Result<T>,失败显式上抛不吞错。
#[async_trait]
pub trait TunnelClient: Send + Sync {
/// 主动连接云后端(出站长连接,穿 NAT)
///
/// `url` 形如 `wss://your-server/ws/device?token=xxx`。
/// 成功后进入「已连接」状态,失败返回 TunnelError::Connect。
async fn connect(&self, url: &str) -> Result<()>;
/// 主动断开(优雅关闭 WS,释放底层 TCP)
async fn disconnect(&self) -> Result<()>;
/// 是否处于已连接状态(轻量查询,不阻塞,不发心跳)
fn is_connected(&self) -> bool;
/// 发送事件(桌面端 → 云后端 → 小程序)
///
/// 设计为按事件粒度发送,不聚合;调用方在 app.emit 的同时 clone 推隧道。
async fn send_event(&self, event: TunnelEvent) -> Result<()>;
/// 接收指令(云后端 → 桌面端)
///
/// 阻塞等待下一条指令。返回 None 表示连接已关闭(EOF)。
async fn recv_command(&self) -> Result<Option<TunnelCommand>>;
}
/// 默认 WS 隧道客户端骨架(tokio-tungstenite)
///
/// 脚手架阶段:字段占位,方法返回 `TunnelError::Other("未实现")`。
/// Phase2 替换为完整实现:
/// - `WebSocketStream<MaybeTlsStream<TcpStream>>` 持有连接
/// - `mpsc` 通道解耦 send/recv 任务
/// - 鉴权握手 + 心跳 + 重连
pub struct WsTunnelClient {
/// 目标服务器 URL(连接后保存,重连用)
#[allow(dead_code)]
server_url: Option<String>,
/// 连接状态标志(轻量 is_connected 查询用)
#[allow(dead_code)]
connected: bool,
}
impl WsTunnelClient {
/// 创建未连接的客户端实例
pub fn new() -> Self {
Self {
server_url: None,
connected: false,
}
}
}
impl Default for WsTunnelClient {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl TunnelClient for WsTunnelClient {
async fn connect(&self, url: &str) -> Result<()> {
// 脚手架占位:Phase2 实现 tokio_tungstenite::connect_async + 鉴权握手。
// 此处仅记录意图,返回未实现错误(不静默假成功,避免掩盖调用方逻辑)。
tracing::warn!(url = %url, "WsTunnelClient.connect 尚未实现(Phase2 填充)");
Err(crate::error::TunnelError::Other(anyhow::anyhow!(
"WsTunnelClient.connect 尚未实现(Phase2)"
)))
}
async fn disconnect(&self) -> Result<()> {
tracing::warn!("WsTunnelClient.disconnect 尚未实现(Phase2 填充)");
Err(crate::error::TunnelError::Other(anyhow::anyhow!(
"WsTunnelClient.disconnect 尚未实现(Phase2)"
)))
}
fn is_connected(&self) -> bool {
self.connected
}
async fn send_event(&self, _event: TunnelEvent) -> Result<()> {
Err(crate::error::TunnelError::Other(anyhow::anyhow!(
"WsTunnelClient.send_event 尚未实现(Phase2)"
)))
}
async fn recv_command(&self) -> Result<Option<TunnelCommand>> {
Err(crate::error::TunnelError::Other(anyhow::anyhow!(
"WsTunnelClient.recv_command 尚未实现(Phase2)"
)))
}
}