重构: df-tunnel Phase3阶段1纯透传(D1=A:CommandHandler收Value+payload透传不解析TunnelCommand,D5保留弱校验)
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
//! `{"kind":"control","error":"..."}` + Close,成功则静默进入收发循环(无显式 ack)
|
||||
//! - 握手成功信号:**未收到 error 帧 且 socket 保持打开**(connect 用 timeout 探测首帧)
|
||||
//! - 入站消息:relay 把对端 Miniapp 的文本帧包成 BroadcastMessage 转发,桌面端收到的
|
||||
//! 是完整 BroadcastMessage JSON,payload 才是真正的 TunnelCommand
|
||||
//! 是完整 BroadcastMessage JSON,payload 是 miniapp 指令原样 JSON(Phase3 纯透传,不解析)
|
||||
//! - 出站消息:桌面端发 TunnelEvent 的 JSON(relay 把它当 payload 包成 BroadcastMessage)
|
||||
//!
|
||||
//! ## AiChatEvent 透传原则
|
||||
@@ -18,7 +18,8 @@
|
||||
//!
|
||||
//! ## 并发模型
|
||||
//! 内部 spawn 收发循环 task,通过 mpsc 解耦 send_event(调用方)与 socket 写入。
|
||||
//! 收到 TunnelCommand 走 on_command 回调(调用方在 connect 时注册),不阻塞收发循环。
|
||||
//! 收到 command 方向 payload(`serde_json::Value`)走 on_command 回调(调用方在 connect 时注册),不阻塞收发循环。
|
||||
//! Phase3 协议统一(D1=A):tunnel 纯透传,业务协议解析在 device 端桥接层(src-tauri)。
|
||||
//! `&self` + `Arc<Mutex<...>>` 持连接状态,trait 方法全部 async + 不持 stream 借用。
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -33,13 +34,15 @@ use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||
use tokio_tungstenite::tungstenite::Message as WsMessage;
|
||||
|
||||
use crate::error::{Result, TunnelError};
|
||||
use crate::events::{Hello, HelloKind, RelayControlError, TunnelCommand, TunnelEvent};
|
||||
use crate::events::{Hello, HelloKind, RelayControlError, TunnelEvent};
|
||||
|
||||
/// 收到 TunnelCommand 时的回调类型(Boxed Future,在收发循环 task 内 await)
|
||||
/// 收到 miniapp→device 指令 payload 时的回调类型(Boxed Future,在收发循环 task 内 await)
|
||||
///
|
||||
/// Phase3 协议统一(D1=A):tunnel 纯透传,不再反序列化 TunnelCommand 强类型,
|
||||
/// payload 以 `serde_json::Value` 上抛,业务协议解析在 device 端桥接层(src-tauri)。
|
||||
/// 设计为 async 便于调用方执行 Tauri command 异步路由。回调内 panic 会中断收发循环,
|
||||
/// 调用方应自行处理错误(回调返回值忽略,失败由调用方记录)。
|
||||
pub type CommandHandler = Arc<dyn Fn(TunnelCommand) -> futures_util::future::BoxFuture<'static, ()> + Send + Sync>;
|
||||
pub type CommandHandler = Arc<dyn Fn(serde_json::Value) -> futures_util::future::BoxFuture<'static, ()> + Send + Sync>;
|
||||
|
||||
/// 隧道客户端抽象
|
||||
///
|
||||
@@ -58,7 +61,8 @@ pub trait TunnelClient: Send + Sync {
|
||||
///
|
||||
/// `url` 形如 `wss://host/ws/device`(query 不带 token,token 走 Hello 帧)。
|
||||
/// 成功后进入「已连接」状态并 spawn 收发循环。失败返回 TunnelError::Connect/Auth。
|
||||
/// `on_command` 收到云后端转发的 TunnelCommand 时被调用(在收发 task 内异步执行)。
|
||||
/// `on_command` 收到云后端转发的 command 方向 payload Value 时被调用(在收发 task 内异步执行)。
|
||||
/// Phase3 纯透传(D1=A):payload 不在 tunnel 解析,由 device 端桥接层 match cmd 路由。
|
||||
async fn connect(
|
||||
&self,
|
||||
url: &str,
|
||||
@@ -294,7 +298,7 @@ impl WsTunnelClient {
|
||||
/// 收发循环主体(spawn 后独立运行)
|
||||
///
|
||||
/// 职责:
|
||||
/// 1. socket 入帧 → 解析 BroadcastMessage → 取 payload 反序列化 TunnelCommand → on_command 回调
|
||||
/// 1. socket 入帧 → 解析 BroadcastMessage → 取 payload Value 纯透传 → on_command 回调(协议解析在桥接层)
|
||||
/// 2. mpsc 出帧 → socket 写入(Event 序列化 / Ping 心跳 / Close 关闭)
|
||||
/// 3. 心跳定时器:每 HEARTBEAT_INTERVAL 发一次 Ping
|
||||
/// 4. 任一端断开 → 退出 task,置 connected=false
|
||||
@@ -400,16 +404,12 @@ async fn run_loop(
|
||||
async fn handle_inbound(msg: &WsMessage, on_command: &CommandHandler) -> bool {
|
||||
match msg {
|
||||
WsMessage::Text(text) => {
|
||||
// relay 转发的是完整 BroadcastMessage JSON
|
||||
// (含 device_id/kind/source/from/payload/ts,payload 才是 TunnelCommand)
|
||||
// 先尝试按 BroadcastMessage 镜像解析取 payload;失败则尝试直接解析为 TunnelCommand
|
||||
// (兼容 relay 未来直发 payload 的变更)
|
||||
let cmd_opt = parse_command_from_broadcast(text)
|
||||
.or_else(|| serde_json::from_str::<TunnelCommand>(text).ok());
|
||||
|
||||
if let Some(cmd) = cmd_opt {
|
||||
// 回调内执行指令路由(Tauri command);回调失败不影响收发循环
|
||||
let fut = on_command(cmd);
|
||||
// Phase3 纯透传(D1=A):relay 转发完整 BroadcastMessage,
|
||||
// payload 是 miniapp 指令原样 JSON。tunnel 只提取 payload Value 上抛,
|
||||
// 不解析业务协议(协议解析在 device 端桥接层 src-tauri)。
|
||||
if let Some(payload) = parse_payload_from_broadcast(text) {
|
||||
// 回调内执行指令路由(桥接层 match cmd → Tauri command);回调失败不影响收发循环
|
||||
let fut = on_command(payload);
|
||||
fut.await;
|
||||
} else {
|
||||
// 非 Command 消息(Control/Event 回环/未知):忽略,不中断
|
||||
@@ -431,12 +431,12 @@ async fn handle_inbound(msg: &WsMessage, on_command: &CommandHandler) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 BroadcastMessage JSON 提取 TunnelCommand
|
||||
/// 从 BroadcastMessage JSON 提取 command 方向的 payload Value(纯透传,不解析业务协议)
|
||||
///
|
||||
/// df-relay 入站包成 `BroadcastMessage { ..., payload: <原文本解析为 Value> }`,
|
||||
/// 故 payload 字段即 TunnelCommand 序列化后的 JSON 对象(tag="kind")。
|
||||
/// 此处只镜像解析 payload 字段(避免依赖 df-relay crate),payload 用 Value 透传不耦合。
|
||||
fn parse_command_from_broadcast(raw: &str) -> Option<TunnelCommand> {
|
||||
/// Phase3 协议统一(D1=A):df-relay 入站包成 `BroadcastMessage { ..., payload: Value }`,
|
||||
/// tunnel 只提取 payload 字段以 Value 上抛 on_command,不反序列化为 TunnelCommand 强类型
|
||||
/// (业务协议解析在 device 端桥接层)。仅 kind=="command" 时提取(Event/Control 不提,避免回环噪音)。
|
||||
fn parse_payload_from_broadcast(raw: &str) -> Option<serde_json::Value> {
|
||||
// 仅取 payload 字段,避整结构强类型耦合(device_id/source 等字段本客户端不关心)
|
||||
#[derive(serde::Deserialize)]
|
||||
struct BroadcastLike {
|
||||
@@ -446,12 +446,11 @@ fn parse_command_from_broadcast(raw: &str) -> Option<TunnelCommand> {
|
||||
kind: Option<String>,
|
||||
}
|
||||
let parsed: BroadcastLike = serde_json::from_str(raw).ok()?;
|
||||
// 仅 kind == "command" 时才尝试提 TunnelCommand(Event/Control 不提)
|
||||
// 仅 kind == "command" 时才提 payload(Event/Control 不提)
|
||||
if parsed.kind.as_deref() != Some("command") {
|
||||
return None;
|
||||
}
|
||||
let payload = parsed.payload?;
|
||||
serde_json::from_value::<TunnelCommand>(payload).ok()
|
||||
parsed.payload
|
||||
}
|
||||
|
||||
/// 判断是否为 relay 控制面错误帧
|
||||
@@ -486,10 +485,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_from_broadcast_extracts_payload() {
|
||||
// 模拟 relay 转发的 BroadcastMessage
|
||||
let raw = r#"{"device_id":"dev-1","kind":"command","source":{"0":42},"from":"miniapp","payload":{"kind":"send","conv_id":"c1","content":"hi"},"ts":1700000000000}"#;
|
||||
let cmd = parse_command_from_broadcast(raw).expect("应提取出 TunnelCommand");
|
||||
fn parse_payload_from_broadcast_extracts_value() {
|
||||
// 模拟 relay 转发的 BroadcastMessage(miniapp 发的 MiniCommand {cmd, args} 在 payload)
|
||||
let raw = r#"{"device_id":"dev-1","kind":"command","source":{"0":42},"from":"miniapp","payload":{"cmd":"send_message","args":{"message":"hi","conversation_id":"c1"}},"ts":1700000000000}"#;
|
||||
let payload = parse_payload_from_broadcast(raw).expect("应提取出 payload Value");
|
||||
// 纯透传:payload 原样上抛,字段保持 miniapp 端 {cmd, args} 结构(未做协议转换)
|
||||
assert_eq!(payload["cmd"], "send_message");
|
||||
assert_eq!(payload["args"]["message"], "hi");
|
||||
assert_eq!(payload["args"]["conversation_id"], "c1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_payload_ignores_event_kind() {
|
||||
// Event 方向(device→miniapp)不提取,避免回环噪音
|
||||
let raw = r#"{"device_id":"dev-1","kind":"event","source":{"0":1},"from":"device","payload":{"type":"text_delta","conversation_id":"c1","delta":"x"},"ts":1}"#;
|
||||
assert!(parse_payload_from_broadcast(raw).is_none());
|
||||
}
|
||||
|
||||
/// D5 弱校验:payload Value 仍可反序列化为 TunnelCommand(保留强类型作桥接层可选校验)
|
||||
#[test]
|
||||
fn payload_compatible_with_tunnel_command() {
|
||||
use crate::events::TunnelCommand;
|
||||
let raw = r#"{"device_id":"dev-1","kind":"command","source":{"0":42},"from":"miniapp","payload":{"kind":"send","conv_id":"c1","content":"hi"},"ts":1}"#;
|
||||
let payload = parse_payload_from_broadcast(raw).expect("应提取 payload");
|
||||
let cmd: TunnelCommand = serde_json::from_value(payload).expect("payload 应兼容 TunnelCommand");
|
||||
match cmd {
|
||||
TunnelCommand::Send { conv_id, content } => {
|
||||
assert_eq!(conv_id, "c1");
|
||||
@@ -499,12 +518,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_ignores_event_kind() {
|
||||
let raw = r#"{"device_id":"dev-1","kind":"event","source":{"0":1},"from":"device","payload":{"kind":"text_delta","conv_id":"c1","delta":"x"},"ts":1}"#;
|
||||
assert!(parse_command_from_broadcast(raw).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_relay_error_detects_auth_failure() {
|
||||
let msg = WsMessage::Text(r#"{"kind":"control","error":"auth_failed"}"#.into());
|
||||
|
||||
Reference in New Issue
Block a user