新增: 跨端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:
19
crates/df-relay/Cargo.toml
Normal file
19
crates/df-relay/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "df-relay"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "跨端 AI Chat 云中继服务(axum WS Server,转发小程序 ↔ 桌面端)"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# HTTP + WS 服务端
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
tokio-tungstenite = { version = "0.23", features = ["native-tls"] }
|
||||
futures-util = "0.3"
|
||||
85
crates/df-relay/src/broadcast.rs
Normal file
85
crates/df-relay/src/broadcast.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! df-relay 广播消息骨架
|
||||
//!
|
||||
//! 设计依据:设计文档「Layer2」—— 云后端纯转发,无业务逻辑。
|
||||
//! 中继消息骨架定义「谁发给谁」的路由语义:
|
||||
//! - `DeviceEvent`:桌面端事件 → 广播给该 device 绑定的小程序
|
||||
//! - `MiniappCommand`:小程序指令 → 转发给该 device 绑定的桌面端
|
||||
//! - `Control`:控制面消息(配对/心跳/在线状态)
|
||||
//!
|
||||
//! 与 df-tunnel::TunnelMessage 区分:
|
||||
//! - TunnelMessage 是隧道两端(桌面↔云)的传输单元
|
||||
//! - BroadcastMessage 是云后端内部的路由单元(标识来源/去向/广播范围)
|
||||
//!
|
||||
//! 脚手架阶段仅定义 enum,Phase2 填路由表 + 广播实现。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 客户端类型(用于路由判断)
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ClientKind {
|
||||
/// 桌面端(出站长连接,事件真相源)
|
||||
Device,
|
||||
/// 小程序(远程操作发起方)
|
||||
Miniapp,
|
||||
}
|
||||
|
||||
/// 中继路由消息骨架
|
||||
///
|
||||
/// 每条消息携带 `device_id`(配对绑定键),云后端按 device_id
|
||||
/// 找到绑定的对端连接进行转发/广播。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BroadcastMessage {
|
||||
/// 配对绑定的设备 ID(桌面端首次配置时生成)
|
||||
pub device_id: String,
|
||||
/// 消息来源(用于鉴权校验:device 不能发 MiniappCommand)
|
||||
pub from: ClientKind,
|
||||
/// 消息载荷(原始 JSON,转发时不解析业务字段,保持轻量)
|
||||
pub payload: serde_json::Value,
|
||||
/// 时间戳(毫秒,用于冲突处理 —— 桌面端为真相源,但保留时间戳辅助判断)
|
||||
pub ts: i64,
|
||||
}
|
||||
|
||||
impl BroadcastMessage {
|
||||
/// 便捷构造:device 发出的事件
|
||||
pub fn from_device(device_id: impl Into<String>, payload: serde_json::Value, ts: i64) -> Self {
|
||||
Self {
|
||||
device_id: device_id.into(),
|
||||
from: ClientKind::Device,
|
||||
payload,
|
||||
ts,
|
||||
}
|
||||
}
|
||||
|
||||
/// 便捷构造:小程序发出的指令
|
||||
pub fn from_miniapp(device_id: impl Into<String>, payload: serde_json::Value, ts: i64) -> Self {
|
||||
Self {
|
||||
device_id: device_id.into(),
|
||||
from: ClientKind::Miniapp,
|
||||
payload,
|
||||
ts,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 客户端在线状态(用于离线降级提示)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PresenceState {
|
||||
/// 在线
|
||||
Online,
|
||||
/// 离线(对端可缓存指令待重连)
|
||||
Offline,
|
||||
}
|
||||
|
||||
/// 控制面消息(配对绑定 / 心跳 / 在线状态广播)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ControlMessage {
|
||||
/// 配对绑定请求(小程序持 token 申请绑定 device_id)
|
||||
Pair { device_id: String, token: String },
|
||||
/// 心跳(保活 + 在线确认)
|
||||
Heartbeat { device_id: String },
|
||||
/// 在线状态变更通知
|
||||
Presence { device_id: String, state: PresenceState },
|
||||
}
|
||||
37
crates/df-relay/src/error.rs
Normal file
37
crates/df-relay/src/error.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! df-relay 错误类型
|
||||
//!
|
||||
//! 设计:单一 RelayError 覆盖服务启动/客户端管理/广播/WS 协议四类失败,
|
||||
//! thiserror 派生保留错误链。脚手架阶段仅定义类型。
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// 中继服务运行期错误
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RelayError {
|
||||
/// 服务启动失败(端口占用 / 绑定失败)
|
||||
#[error("中继服务启动失败: {0}")]
|
||||
Start(String),
|
||||
|
||||
/// 客户端管理错误(配对失败 / 重复连接 / 鉴权拒绝)
|
||||
#[error("客户端管理错误: {0}")]
|
||||
Client(String),
|
||||
|
||||
/// 广播失败(无可用连接 / 发送失败)
|
||||
#[error("广播失败: {0}")]
|
||||
Broadcast(String),
|
||||
|
||||
/// WS 协议层错误(底层 tungstenite)
|
||||
#[error("WS 协议错误: {0}")]
|
||||
WebSocket(String),
|
||||
|
||||
/// 序列化/反序列化失败
|
||||
#[error("消息序列化失败: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
|
||||
/// 其他未归类错误(降级通道)
|
||||
#[error("中继内部错误: {0}")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// 模块级 Result 别名
|
||||
pub type Result<T> = std::result::Result<T, RelayError>;
|
||||
26
crates/df-relay/src/lib.rs
Normal file
26
crates/df-relay/src/lib.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
//! # df-relay
|
||||
//!
|
||||
//! 跨端 AI Chat 云中继服务(设计文档 Layer2)。
|
||||
//!
|
||||
//! ## 职责
|
||||
//! - axum WS Server 接受两类连接:小程序(device_id 鉴权)+ 桌面端(token 配对)
|
||||
//! - 广播中继:小程序操作 → 桌面端;桌面端事件 → 小程序
|
||||
//! - 纯转发,无业务逻辑(保持轻量,设计文档明确)
|
||||
//!
|
||||
//! ## 边界
|
||||
//! - 不依赖 df-tunnel / df-types / src-tauri(独立消息骨架,避免跨 crate 强耦合)
|
||||
//! - 不含业务逻辑(Phase2 填连接生命周期 / 注册表 / 广播分发)
|
||||
//!
|
||||
//! ## 模块
|
||||
//! - [`broadcast`]:广播消息骨架(BroadcastMessage / ClientKind / ControlMessage)
|
||||
//! - [`relay`]:RelayServer trait + DefaultRelayServer 骨架 + axum WS 路由
|
||||
//! - [`error`]:RelayError 错误类型
|
||||
|
||||
pub mod broadcast;
|
||||
pub mod error;
|
||||
pub mod relay;
|
||||
|
||||
// 顶层再导出常用项
|
||||
pub use broadcast::{BroadcastMessage, ClientKind, ControlMessage, PresenceState};
|
||||
pub use error::{RelayError, Result};
|
||||
pub use relay::{build_router, DefaultRelayServer, RelayServer, RelayState};
|
||||
158
crates/df-relay/src/relay.rs
Normal file
158
crates/df-relay/src/relay.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
//! df-relay 中继服务骨架
|
||||
//!
|
||||
//! 设计依据:设计文档「Layer2」—— axum WS Server + 广播中继。
|
||||
//! 接受两类连接:小程序(device_id 鉴权)+ 桌面端(token 配对)。
|
||||
//! 纯转发,无业务逻辑。
|
||||
//!
|
||||
//! 脚手架范围:RelayServer trait + axum WS 路由骨架(占位 handler),
|
||||
//! Phase2 填:
|
||||
//! - 客户端注册表(HashMap<device_id, 连接集>)
|
||||
//! - 广播分发(按 device_id 路由)
|
||||
//! - 鉴权中间件
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{WebSocket, WebSocketUpgrade},
|
||||
State,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::broadcast::BroadcastMessage;
|
||||
use crate::error::Result;
|
||||
|
||||
/// 中继服务抽象
|
||||
///
|
||||
/// 设计为 trait 便于测试 mock + 未来替换实现(如换 tonic gRPC 网关)。
|
||||
#[async_trait]
|
||||
pub trait RelayServer: Send + Sync {
|
||||
/// 启动 HTTP/WS 服务监听指定地址
|
||||
async fn start(&self, addr: &str) -> Result<()>;
|
||||
|
||||
/// 广播消息给指定 device_id 绑定的对端
|
||||
async fn broadcast(&self, msg: BroadcastMessage) -> Result<()>;
|
||||
|
||||
/// 注册新连接(连接建立后入表)
|
||||
async fn register_client(&self, device_id: String) -> Result<()>;
|
||||
|
||||
/// 注销连接(断开后出表)
|
||||
async fn unregister_client(&self, device_id: String) -> Result<()>;
|
||||
|
||||
/// 查询 device 是否有在线连接(离线降级判断用)
|
||||
fn is_device_online(&self, device_id: &str) -> bool;
|
||||
}
|
||||
|
||||
/// 共享状态(axum State 传递)
|
||||
///
|
||||
/// 脚手架占位:Phase2 填 tokio::sync::RwLock<HashMap<device_id, ConnectionSet>>。
|
||||
/// 当前用 unit 占位以满足 Router state 约束。
|
||||
#[derive(Clone, Default)]
|
||||
pub struct RelayState {
|
||||
/// Phase2:客户端注册表
|
||||
/// 例:`clients: Arc<RwLock<HashMap<String, ConnectionSet>>>`
|
||||
#[allow(dead_code)]
|
||||
_reserved: (),
|
||||
}
|
||||
|
||||
/// axum WS 路由构造
|
||||
///
|
||||
/// 暴露 `/ws/device`(桌面端连入)与 `/ws/miniapp`(小程序连入)两个端点,
|
||||
/// 共享 RelayState。Phase2 在 handler 内填鉴权 + 连接生命周期管理。
|
||||
pub fn build_router(state: Arc<RelayState>) -> Router {
|
||||
Router::new()
|
||||
.route("/ws/device", get(device_ws_handler))
|
||||
.route("/ws/miniapp", get(miniapp_ws_handler))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// 桌面端 WS 连接 handler(脚手架占位)
|
||||
///
|
||||
/// 当前仅接受升级并立即返回(不消费流),Phase2 填:
|
||||
/// - 握手鉴权(token)
|
||||
/// - register_client
|
||||
/// - 循环 recv 转发 BroadcastMessage
|
||||
/// - 断开 unregister_client
|
||||
async fn device_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(_state): State<Arc<RelayState>>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("桌面端 WS 连接接入(脚手架占位)");
|
||||
ws.on_upgrade(handle_connection)
|
||||
}
|
||||
|
||||
/// 小程序 WS 连接 handler(脚手架占位)
|
||||
async fn miniapp_ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(_state): State<Arc<RelayState>>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("小程序 WS 连接接入(脚手架占位)");
|
||||
ws.on_upgrade(handle_connection)
|
||||
}
|
||||
|
||||
/// 通用 WS 连接处理(脚手架:接收一次即关闭)
|
||||
///
|
||||
/// Phase2 替换为完整读写循环 + 广播分发。
|
||||
async fn handle_connection(mut socket: WebSocket) {
|
||||
// 接收首条消息(占位),Phase2 改为持续循环
|
||||
if let Some(Ok(msg)) = futures_util::StreamExt::next(&mut socket).await {
|
||||
tracing::debug!(?msg, "收到首条消息(脚手架占位,Phase2 填路由)");
|
||||
}
|
||||
// 脚手架:静默关闭,不报错(连接生命周期管理留 Phase2)
|
||||
}
|
||||
|
||||
/// 默认中继服务骨架
|
||||
///
|
||||
/// 脚手架阶段:trait 方法返回未实现错误,不静默假成功。
|
||||
/// Phase2 替换为完整实现(启动 axum::serve + 注册表 + 广播)。
|
||||
pub struct DefaultRelayServer {
|
||||
#[allow(dead_code)]
|
||||
bind_addr: Option<String>,
|
||||
}
|
||||
|
||||
impl DefaultRelayServer {
|
||||
pub fn new() -> Self {
|
||||
Self { bind_addr: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DefaultRelayServer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RelayServer for DefaultRelayServer {
|
||||
async fn start(&self, addr: &str) -> Result<()> {
|
||||
tracing::warn!(addr = %addr, "DefaultRelayServer.start 尚未实现(Phase2 填充)");
|
||||
Err(crate::error::RelayError::Other(anyhow::anyhow!(
|
||||
"DefaultRelayServer.start 尚未实现(Phase2)"
|
||||
)))
|
||||
}
|
||||
|
||||
async fn broadcast(&self, _msg: BroadcastMessage) -> Result<()> {
|
||||
Err(crate::error::RelayError::Other(anyhow::anyhow!(
|
||||
"DefaultRelayServer.broadcast 尚未实现(Phase2)"
|
||||
)))
|
||||
}
|
||||
|
||||
async fn register_client(&self, _device_id: String) -> Result<()> {
|
||||
Err(crate::error::RelayError::Other(anyhow::anyhow!(
|
||||
"DefaultRelayServer.register_client 尚未实现(Phase2)"
|
||||
)))
|
||||
}
|
||||
|
||||
async fn unregister_client(&self, _device_id: String) -> Result<()> {
|
||||
Err(crate::error::RelayError::Other(anyhow::anyhow!(
|
||||
"DefaultRelayServer.unregister_client 尚未实现(Phase2)"
|
||||
)))
|
||||
}
|
||||
|
||||
fn is_device_online(&self, _device_id: &str) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user