优化: 跨端链路协议对齐
- miniapp 配置域旧端点强制重置 - df-tunnel 隧道协议与 relay 现网对齐 - relay e2e 例程入 examples
This commit is contained in:
@@ -1,9 +1,26 @@
|
||||
# df-relay — 已迁移(deprecated)
|
||||
|
||||
> **本 crate 已于 2026-08-23 提取为独立产品 [`u-relay`](../u-relay)**(`E:/wk-lab/u-relay`,git filter-repo 保留完整提交历史,脱敏改名 df_\*→u_\*)。
|
||||
> **本 crate 已于 2026-08-23 提取为独立产品 [`u-relay`](../../../u-relay)**(`E:/wk-lab/u-relay`,git filter-repo 保留完整提交历史,脱敏改名 df_\*→u_\*)。
|
||||
|
||||
- **新功能一律在 u-relay 仓库开发**,本 crate 不再接受功能演进(防双份漂移)
|
||||
- 本目录暂保留:现役生产部署(容器 :9180)仍从 devflow 侧构建,待 devflow 客户端(df-tunnel / 小程序 ws.ts)迁移 u-relay 后整体下线(设计 Q7)
|
||||
- u-relay 协议为 df-relay 超集:hello / device_id / Event / Command 帧原样兼容,迁移只需切 host
|
||||
- 本目录暂保留:现役生产部署(容器 :9180)仍从 devflow 侧构建,待 devflow 客户端(df-tunnel / df-miniapp ws.ts)迁移 u-relay 后整体下线(设计 Q7)
|
||||
|
||||
## ⚠️ 协议兼容性(2026-10-04 更正)
|
||||
|
||||
早期陈述「u-relay 协议为 df-relay 超集,hello/device_id 原样兼容,迁移只需切 host」**已过时**。
|
||||
u-relay 在协议正名(agent/commander 第三代)时**已拆除全部兼容层**:
|
||||
|
||||
| 维度 | df-relay(本 crate) | u-relay(现行) |
|
||||
|---|---|---|
|
||||
| 端点 | `/ws/device`、`/ws/miniapp` | **`/ws/agent`、`/ws/commander`**(旧端点不存在) |
|
||||
| Hello.kind | `"device"`、`"miniapp"` | **`"agent"`、`"commander"`**(旧 kind 握手期直接拒绝) |
|
||||
| 配对键字段 | `device_id` | **`channel_id`**(不再 alias) |
|
||||
| 错误码 | `invalid_device_id` | `invalid_channel_id` |
|
||||
| token env | `DF_RELAY_TOKEN` | `U_RELAY_TOKEN` |
|
||||
|
||||
**迁移不是"切 host"而是协议升级**。devflow 侧客户端已于 2026-10-04 完成升级:
|
||||
- `crates/df-tunnel`(Rust,桌面端 Agent)✅
|
||||
- `apps/df-miniapp`(uni-app,Commander:config.ts / ws.ts / types/relay.ts / settings 页 + storage 旧字段迁移)✅
|
||||
|
||||
协议规格见 `docs/02-架构设计/专项设计/u-relay-wire-protocol-2026-10-04.md`。
|
||||
设计文档:`C:/Users/23780/docs/u-box-design.md` §3.3 + `u-relay-extract-plan.md`
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! df-tunnel ↔ u-relay 端到端联调(Agent 侧握手 + 存活 + 断开)
|
||||
//!
|
||||
//! 起 WsTunnelClient 连本机 u-relay :19180 /ws/agent:
|
||||
//! 1. Hello{kind:"agent", channel_id, token} → u-relay 立即回 hello_ack(3s PROBE 内收到)
|
||||
//! 2. send_raw_event 发一条事件(relay 无 commander 在线 → 静默丢弃,不报错)
|
||||
//! 3. 存活 2s(收发循环稳定,无 panic)
|
||||
//! 4. 优雅 disconnect
|
||||
//!
|
||||
//! 运行:先起 u-relay(U_RELAY_TOKEN=devflow-relay-default-token U_RELAY_ADDR=127.0.0.1:19180),
|
||||
//! 再 `cargo run -p df-tunnel --example e2e_u_relay`。
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use df_tunnel::tunnel::CommandHandler;
|
||||
use df_tunnel::{TunnelClient, WsTunnelClient};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let url = "ws://127.0.0.1:19180/ws/agent";
|
||||
let channel_id = "test-channel-e2e-01";
|
||||
let token = "devflow-relay-default-token";
|
||||
|
||||
let client = Arc::new(WsTunnelClient::new());
|
||||
|
||||
// 注册 command 回调(收 commander 指令;本测试预计不触发)
|
||||
let got_cmd = Arc::new(Mutex::new(0usize));
|
||||
let counter = got_cmd.clone();
|
||||
let on_command: CommandHandler = Arc::new(move |_payload| {
|
||||
let counter = counter.clone();
|
||||
Box::pin(async move {
|
||||
*counter.lock().await += 1;
|
||||
})
|
||||
});
|
||||
|
||||
println!("[1] connect {url} channel={channel_id}");
|
||||
client
|
||||
.connect(url, channel_id, token, on_command)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("connect 失败: {e}"))?;
|
||||
println!(" connect Ok");
|
||||
|
||||
assert!(client.is_connected(), "is_connected 应为 true");
|
||||
println!("[2] is_connected == true ✓(u-relay 已回 hello_ack,握手成功)");
|
||||
|
||||
// 发一条原始事件(relay 无 commander 在线 → 静默丢弃,不应报错)
|
||||
client
|
||||
.send_raw_event(serde_json::json!({"type": "e2e_probe", "note": "df-tunnel e2e"}))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("send_raw_event 失败: {e}"))?;
|
||||
println!("[3] send_raw_event Ok(事件出站;relay 无对端静默丢弃符合预期)");
|
||||
|
||||
// 保持 2s 让心跳/收发循环稳定跑(无 panic 即可)
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
assert!(client.is_connected(), "2s 后仍应 connected(心跳保活)");
|
||||
println!("[4] 2s 存活 ✓");
|
||||
|
||||
// 优雅断开
|
||||
client.disconnect().await?;
|
||||
assert!(!client.is_connected(), "disconnect 后 is_connected 应为 false");
|
||||
println!("[5] disconnect Ok,is_connected == false ✓");
|
||||
|
||||
let got = *got_cmd.lock().await;
|
||||
println!("───");
|
||||
println!("df-tunnel ↔ u-relay E2E: 5/5 通过(command 收到 {got} 条,预期 0)");
|
||||
Ok(())
|
||||
}
|
||||
@@ -11,41 +11,51 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 握手身份宣告帧(对齐 df-relay::relay::Hello)
|
||||
/// 握手身份宣告帧(对齐 u-relay::relay::Hello)
|
||||
///
|
||||
/// 桌面端 WS 连 `/ws/device` 后,首条消息发此 JSON,relay 校验 kind 与 token。
|
||||
/// 字段名/serde rename 与 df-relay 严格一致(`#[serde(rename_all="snake_case")]`),
|
||||
/// 桌面端 WS 连 `/ws/agent` 后,首条消息发此 JSON,relay 校验 kind 与 token。
|
||||
/// 字段名/serde rename 与 u-relay 严格一致(`#[serde(rename_all="snake_case")]`),
|
||||
/// 否则 relay 反序列化失败 → 握手被拒。
|
||||
///
|
||||
/// 注意:本 crate 不依赖 df-relay(避免 lib→server 反向依赖),自行镜像协议结构。
|
||||
/// 协议字段如有变更须同步 df-relay::relay::Hello 与本类型。
|
||||
/// 注意:本 crate 不依赖 u-relay(避免 lib→server 反向依赖),自行镜像协议结构。
|
||||
/// 协议字段如有变更须同步 u-relay::relay::Hello 与本类型。
|
||||
///
|
||||
/// 历史:df-relay 一代协议用 device_id + kind:"device",已在 u-relay 现行版全部下线
|
||||
/// (端点 /ws/agent,kind:"agent",配对键 channel_id,旧名握手期直接拒绝)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Hello {
|
||||
/// 客户端类型("device" / "miniapp"),桌面端恒为 Device
|
||||
/// 客户端类型("agent" / "commander"),桌面端恒为 Agent
|
||||
pub kind: HelloKind,
|
||||
/// 配对绑定的设备 ID
|
||||
pub device_id: String,
|
||||
/// 配对 token(env DF_RELAY_TOKEN 同源)
|
||||
/// 配对绑定的通道 ID(非空,≤128B,无控制字符)
|
||||
pub channel_id: String,
|
||||
/// 配对 token(env U_RELAY_TOKEN 同源)
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
/// Hello.kind 传输表示(serde 字符串,与 df-relay::ClientKindWire 一致)
|
||||
/// Hello.kind 传输表示(serde 字符串,与 u-relay::ClientKindWire 一致)
|
||||
///
|
||||
/// 现行仅两值:Agent(执行端,如 df-tunnel)/ Commander(控制端,如 df-miniapp)。
|
||||
/// 历史名 Device/Miniapp/Runner/Console/Executor/Controller 已全部下线。
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HelloKind {
|
||||
Device,
|
||||
Miniapp,
|
||||
Agent,
|
||||
Commander,
|
||||
}
|
||||
|
||||
/// relay 控制面错误帧(握手失败时 relay 发送)
|
||||
///
|
||||
/// relay 在校验失败/连接异常时发 `{"kind":"control","error":"<reason>"}` 文本帧。
|
||||
/// 桌面端据此识别握手失败原因(不依赖 error 帧亦可:连接被 Close 即视为失败)。
|
||||
///
|
||||
/// 现行 u-relay 错误码:handshake_failed / kind_mismatch / invalid_channel_id /
|
||||
/// server_misconfigured / auth_failed。历史 `invalid_device_id` 已下线。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RelayControlError {
|
||||
/// 固定 "control"(用于与正常 BroadcastMessage 区分,relay 错误帧简化为两字段)
|
||||
pub kind: String,
|
||||
/// 错误原因(handshake_failed / kind_mismatch / auth_failed)
|
||||
/// 错误原因(handshake_failed / kind_mismatch / invalid_channel_id /
|
||||
/// server_misconfigured / auth_failed)
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
@@ -112,10 +122,11 @@ impl TunnelEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// 云后端 → 桌面端的操作指令(小程序远程操作映射)
|
||||
/// 云后端 → 桌面端的操作指令(控制端远程操作映射)
|
||||
///
|
||||
/// 对齐设计文档「Layer1 指令路由」:
|
||||
/// send/stop/approve/regenerate/switch 五类操作。
|
||||
/// 由控制端(df-miniapp 等 Commander)发出,经 u-relay 路由到本桌面端 Agent。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum TunnelCommand {
|
||||
@@ -133,8 +144,8 @@ pub enum TunnelCommand {
|
||||
|
||||
/// 隧道传输的消息包装(双向)
|
||||
///
|
||||
/// 区分「桌面端 → 云后端 → 小程序」的事件流(TunnelEvent)
|
||||
/// 与「小程序 → 云后端 → 桌面端」的指令流(TunnelCommand)。
|
||||
/// 区分「桌面端 → u-relay → 控制端」的事件流(TunnelEvent)
|
||||
/// 与「控制端 → u-relay → 桌面端」的指令流(TunnelCommand)。
|
||||
/// 同一 WS 连接复用,靠 kind 标签区分。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "dir", rename_all = "snake_case")]
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
//! 本地 DevFlow 桌面端出站 WS 隧道客户端(设计文档 Layer1)。
|
||||
//!
|
||||
//! ## 职责
|
||||
//! - 主动连接云后端 df-relay(出站穿 NAT,无需端口映射)
|
||||
//! - 事件桥接:桌面端 ai-chat-event 透传至云后端 → 小程序
|
||||
//! - 指令路由:接收小程序远程操作 → 调对应 Tauri command
|
||||
//! - 主动连接云后端 u-relay(出站穿 NAT,无需端口映射)
|
||||
//! - 事件桥接:桌面端 ai-chat-event 透传至云后端 → 控制端(df-miniapp 等)
|
||||
//! - 指令路由:接收控制端远程操作 → 调对应 Tauri command
|
||||
//!
|
||||
//! ## 边界
|
||||
//! - 不依赖 src-tauri(TunnelEvent/TunnelCommand 独立定义,避免跨 crate path 引用二进制)
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
//! df-tunnel 隧道客户端(Phase2 完整实现)
|
||||
//! df-tunnel 隧道客户端(Phase2 完整实现,2026-10-04 切换 u-relay 协议)
|
||||
//!
|
||||
//! 设计依据:设计文档「Layer1」—— 桌面端主动出站连云后端 `wss://host/ws/device`,
|
||||
//! 设计依据:设计文档「Layer1」—— 桌面端主动出站连云后端 `wss://host/ws/agent`,
|
||||
//! 穿 NAT 无需端口映射。tokio-tungstenite WS + Hello 握手 + 收发循环。
|
||||
//! 断线重连由**调用方 supervisor** 负责(src-tauri 已实现指数退避重连,见其 tunnel 初始化处):
|
||||
//! 本 crate 提供 connect/disconnect/is_connected 原语 + [`reconnect_params`] 参数复用 + [`backoff_delay`] 退避工具。
|
||||
//!
|
||||
//! ## 协议对齐(df-relay Phase2 commit 2b8b30e)
|
||||
//! - 桌面端连 `/ws/device`,首帧发 `Hello { kind:"device", device_id, token }`
|
||||
//! - relay 双因子校验(kind == expected_device + token 匹配),失败发
|
||||
//! `{"kind":"control","error":"..."}` + Close,成功则静默进入收发循环(无显式 ack)
|
||||
//! - 握手成功信号:**未收到 error 帧 且 socket 保持打开**(connect 用 timeout 探测首帧)
|
||||
//! - 入站消息:relay 把对端 Miniapp 的文本帧包成 BroadcastMessage 转发,桌面端收到的
|
||||
//! 是完整 BroadcastMessage JSON,payload 是 miniapp 指令原样 JSON(Phase3 纯透传,不解析)
|
||||
//! - 出站消息:桌面端发 TunnelEvent 的 JSON(relay 把它当 payload 包成 BroadcastMessage)
|
||||
//! ## 协议对齐(u-relay 现行,2026-10-04 起)
|
||||
//! - 桌面端连 `/ws/agent`,首帧发 `Hello { kind:"agent", channel_id, token }`
|
||||
//! - relay 双因子校验(kind == agent + token 匹配 + channel_id 格式校验),
|
||||
//! 失败发 `{"kind":"control","error":"..."}` + Close,
|
||||
//! 成功立即回 `{"kind":"control","payload":{"control_kind":"hello_ack"}}` 显式 ack
|
||||
//! - 握手成功信号:**收到 hello_ack 控制帧 或 收到任何非 error 首帧**(u-relay 有显式 ack)
|
||||
//! - 入站消息:relay 把对端 Commander 的文本帧包成 BroadcastMessage 转发,桌面端收到的
|
||||
//! 是完整 BroadcastMessage JSON,payload 是 commander 指令原样 JSON(纯透传,不解析)
|
||||
//! - 出站消息:桌面端发 TunnelEvent 的 JSON(relay 把它当 payload 包成 Event 广播)
|
||||
//! - 心跳:relay 本地回 pong(`{"control_kind":"ping"}` → pong 不经对端),路径短延迟低
|
||||
//!
|
||||
//! ## AiChatEvent 透传原则
|
||||
//! payload 用 serde_json::Value 透传,本 crate 不依赖 src-tauri/df-types(避跨 crate 强耦合)。
|
||||
@@ -21,7 +23,7 @@
|
||||
//! ## 并发模型
|
||||
//! 内部 spawn 收发循环 task,通过 mpsc 解耦 send_event(调用方)与 socket 写入。
|
||||
//! 收到 command 方向 payload(`serde_json::Value`)走 on_command 回调(调用方在 connect 时注册),不阻塞收发循环。
|
||||
//! Phase3 协议统一(D1=A):tunnel 纯透传,业务协议解析在 device 端桥接层(src-tauri)。
|
||||
//! Phase3 协议统一(D1=A):tunnel 纯透传,业务协议解析在 agent 端桥接层(src-tauri)。
|
||||
//! `&self` + `Arc<Mutex<...>>` 持连接状态,trait 方法全部 async + 不持 stream 借用。
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -38,7 +40,7 @@ use tokio_tungstenite::tungstenite::Message as WsMessage;
|
||||
use crate::error::{Result, TunnelError};
|
||||
use crate::events::{Hello, HelloKind, RelayControlError, TunnelEvent};
|
||||
|
||||
/// 收到 miniapp→device 指令 payload 时的回调类型(Boxed Future,在收发循环 task 内 await)
|
||||
/// 收到 commander→agent 指令 payload 时的回调类型(Boxed Future,在收发循环 task 内 await)
|
||||
///
|
||||
/// Phase3 协议统一(D1=A):tunnel 纯透传,不再反序列化 TunnelCommand 强类型,
|
||||
/// payload 以 `serde_json::Value` 上抛,业务协议解析在 device 端桥接层(src-tauri)。
|
||||
@@ -61,14 +63,14 @@ pub type CommandHandler = Arc<dyn Fn(serde_json::Value) -> futures_util::future:
|
||||
pub trait TunnelClient: Send + Sync {
|
||||
/// 主动连接云后端 + 注册指令回调
|
||||
///
|
||||
/// `url` 形如 `wss://host/ws/device`(query 不带 token,token 走 Hello 帧)。
|
||||
/// `url` 形如 `wss://host/ws/agent`(query 不带 token,token 走 Hello 帧)。
|
||||
/// 成功后进入「已连接」状态并 spawn 收发循环。失败返回 TunnelError::Connect/Auth。
|
||||
/// `on_command` 收到云后端转发的 command 方向 payload Value 时被调用(在收发 task 内异步执行)。
|
||||
/// Phase3 纯透传(D1=A):payload 不在 tunnel 解析,由 device 端桥接层 match cmd 路由。
|
||||
/// Phase3 纯透传(D1=A):payload 不在 tunnel 解析,由 agent 端桥接层 match cmd 路由。
|
||||
async fn connect(
|
||||
&self,
|
||||
url: &str,
|
||||
device_id: &str,
|
||||
channel_id: &str,
|
||||
token: &str,
|
||||
on_command: CommandHandler,
|
||||
) -> Result<()>;
|
||||
@@ -79,12 +81,12 @@ pub trait TunnelClient: Send + Sync {
|
||||
/// 是否处于已连接状态(轻量查询,不阻塞,不发心跳)
|
||||
fn is_connected(&self) -> bool;
|
||||
|
||||
/// 发送事件(桌面端 → 云后端 → 小程序)
|
||||
/// 发送事件(桌面端 → 云后端 → 控制端)
|
||||
///
|
||||
/// 非阻塞入队,实际 socket 写入异步完成。连接断开时返回 NotConnected。
|
||||
async fn send_event(&self, event: TunnelEvent) -> Result<()>;
|
||||
|
||||
/// 发送原始事件(桌面端 → 云后端 → 小程序),payload 为 AiChatEvent 序列化 JSON Value
|
||||
/// 发送原始事件(桌面端 → 云后端 → 控制端),payload 为 AiChatEvent 序列化 JSON Value
|
||||
///
|
||||
/// D2 全 19 变体透传:EventBus subscriber 把 AiChatEvent Value 经此方法
|
||||
/// 透传(不经 TunnelEvent 强类型子集)。与 [`send_event`](TunnelClient::send_event) 伴行,
|
||||
@@ -92,7 +94,7 @@ pub trait TunnelClient: Send + Sync {
|
||||
async fn send_raw_event(&self, payload: serde_json::Value) -> Result<()>;
|
||||
}
|
||||
|
||||
/// 握手探测窗口(relay 校验通过后静默进入收发循环,无 ack 帧;
|
||||
/// 握手探测窗口(u-relay 校验通过会立即回 hello_ack 控制帧;
|
||||
/// 此窗口内若收到 error 帧或连接 Close,判定握手失败)
|
||||
const HANDSHAKE_PROBE: Duration = Duration::from_secs(3);
|
||||
|
||||
@@ -104,6 +106,7 @@ const HANDSHAKE_PROBE: Duration = Duration::from_secs(3);
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
/// 心跳间隔(应用层 Ping,补协议层 keepalive,防 NAT 连接表超时回收)
|
||||
/// u-relay 心跳走应用层 control 帧 ping→pong,relay 本地直回不转对端
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(25);
|
||||
|
||||
/// 重连初始退避
|
||||
@@ -136,7 +139,7 @@ pub struct WsTunnelClient {
|
||||
/// 注:本 crate 不内置后台自动重连(见模块文档);重连由调用方 supervisor 轮询
|
||||
/// is_connected + 再 connect 完成(src-tauri 已实现,指数退避)。
|
||||
server_url: Mutex<Option<String>>,
|
||||
device_id: Mutex<Option<String>>,
|
||||
channel_id: Mutex<Option<String>>,
|
||||
token: Mutex<Option<String>>,
|
||||
/// 当前活跃连接状态(None = 未连接)
|
||||
conn: Mutex<Option<ConnState>>,
|
||||
@@ -150,19 +153,19 @@ impl WsTunnelClient {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
server_url: Mutex::new(None),
|
||||
device_id: Mutex::new(None),
|
||||
channel_id: Mutex::new(None),
|
||||
token: Mutex::new(None),
|
||||
conn: Mutex::new(None),
|
||||
connected: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前已连接时取一份重连参数(URL/device_id/token,供调用方 supervisor 断线重连复用)
|
||||
/// 当前已连接时取一份重连参数(URL/channel_id/token,供调用方 supervisor 断线重连复用)
|
||||
pub async fn reconnect_params(&self) -> Option<(String, String, String)> {
|
||||
let url = self.server_url.lock().await.clone()?;
|
||||
let device_id = self.device_id.lock().await.clone()?;
|
||||
let channel_id = self.channel_id.lock().await.clone()?;
|
||||
let token = self.token.lock().await.clone()?;
|
||||
Some((url, device_id, token))
|
||||
Some((url, channel_id, token))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +180,7 @@ impl TunnelClient for WsTunnelClient {
|
||||
async fn connect(
|
||||
&self,
|
||||
url: &str,
|
||||
device_id: &str,
|
||||
channel_id: &str,
|
||||
token: &str,
|
||||
on_command: CommandHandler,
|
||||
) -> Result<()> {
|
||||
@@ -204,8 +207,8 @@ impl TunnelClient for WsTunnelClient {
|
||||
|
||||
// 2. 发 Hello 握手帧
|
||||
let hello = Hello {
|
||||
kind: HelloKind::Device,
|
||||
device_id: device_id.to_string(),
|
||||
kind: HelloKind::Agent,
|
||||
channel_id: channel_id.to_string(),
|
||||
token: token.to_string(),
|
||||
};
|
||||
let hello_json = serde_json::to_string(&hello)?;
|
||||
@@ -216,13 +219,14 @@ impl TunnelClient for WsTunnelClient {
|
||||
// 3. 探测握手结果:HANDSHAKE_PROBE 窗口内
|
||||
// - 收到 error 帧 → relay 拒绝(返回 Auth 错误,附原因)
|
||||
// - 收到 Close / EOF → 连接被关(返回 Connect 错误)
|
||||
// - 收到首条正常消息 / 超时无负帧 → 视为握手成功(relay 静默进入收发循环)
|
||||
// - 收到 hello_ack / 首条正常消息 / 超时无负帧 → 视为握手成功(u-relay 显式 ack)
|
||||
// - Ping/Pong 协议帧:忽略继续等
|
||||
let mut first_frame_pending: Option<WsMessage> = None;
|
||||
let probe = tokio::time::timeout(HANDSHAKE_PROBE, stream.next()).await;
|
||||
match probe {
|
||||
Err(_) => {
|
||||
// 超时无负帧:relay 静默通过(常见路径)
|
||||
// 超时无负帧:hello_ack 错过探测窗(可能性低)或 relay 异常,
|
||||
// 仍按成功处理(后续帧会进 run_loop 正常分派)
|
||||
tracing::debug!("握手探测窗口无负帧,视为成功");
|
||||
}
|
||||
Ok(None) => {
|
||||
@@ -253,7 +257,7 @@ impl TunnelClient for WsTunnelClient {
|
||||
|
||||
// 4. 握手成功:保存参数 + 建发送队列 + spawn 收发循环
|
||||
*self.server_url.lock().await = Some(url.to_string());
|
||||
*self.device_id.lock().await = Some(device_id.to_string());
|
||||
*self.channel_id.lock().await = Some(channel_id.to_string());
|
||||
*self.token.lock().await = Some(token.to_string());
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel::<OutFrame>();
|
||||
@@ -271,7 +275,7 @@ impl TunnelClient for WsTunnelClient {
|
||||
));
|
||||
|
||||
*self.conn.lock().await = Some(ConnState { tx, task });
|
||||
tracing::info!(%url, device_id, "WsTunnelClient 连接就绪,收发循环已启动");
|
||||
tracing::info!(%url, channel_id, "WsTunnelClient 连接就绪,收发循环已启动");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -332,7 +336,7 @@ impl WsTunnelClient {
|
||||
}
|
||||
}
|
||||
*self.server_url.lock().await = None;
|
||||
*self.device_id.lock().await = None;
|
||||
*self.channel_id.lock().await = None;
|
||||
*self.token.lock().await = None;
|
||||
self.connected.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user