Files
DevFlow/docs/02-架构设计/专项设计/u-relay-wire-protocol-2026-10-04.md
T
lxy b994cf2c54 文档: 设计定稿与评估纪律登记
- 无人值守长任务设计定稿,含效率与可观测补遗
- u-relay wire 协议规格与五项评估结论
- 新能力引入评估纪律五项清单
- todo 销账与狗粮短板分析登记
2026-10-04 16:27:32 +08:00

255 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# u-relay Wire 协议规格(改造蓝本)
> 版本:现行(第三代 agent/commander 命名,兼容层全拆)
> 来源:`E:/wk-lab/u-relay/src/{relay.rs, broadcast.rs, conn.rs}` + `README.md`
> 适用:df-tunnel / df-miniapp 升级对接
---
## 1. 拓扑与端点
```
Commander(控制端) ──wss──> nginx(:443 TLS 终结) ──ws──> u-relay(:9180)
↑
Agent(执行端) ──wss 主动出站────────────────────────────┘
(穿 NAT,无监听端口)
```
| 角色 | 端点 | 典型实现 |
|---|---|---|
| **Agent**(执行端,事件真相源) | `GET /ws/agent` | df-tunnel(桌面端)、u-claw runner |
| **Commander**(控制端,指令发起方) | `GET /ws/commander` | df-miniapp(小程序)、u-claw console、u-ppt |
**端点即身份**:端点决定期望的 `ClientKind`,与 Hello.kind 不一致直接拒。
---
## 2. 握手协议
### 2.1 客户端首帧(10s 内必发,否则断开)
```json
{
"kind": "agent" | "commander",
"channel_id": "<非空, ≤128 字节, 无控制字符>",
"token": "<U_RELAY_TOKEN 值>"
}
```
| 字段 | 约束 |
|---|---|
| `kind` | 必须与端点匹配:`/ws/agent` 端点仅收 `"agent"`;`/ws/commander` 端点仅收 `"commander"` |
| `channel_id` | 配对绑定键;非空 + ≤128B + 无控制字符;同 channel_id 的 agent 与 commander 互为对端 |
| `token` | 服务端常量时间比较(防时序侧信道) |
### 2.2 握手成功响应(relay 立即下发)
```json
{ "kind": "control", "payload": { "control_kind": "hello_ack" } }
```
**客户端判定**:收到首条非 error 消息即握手通过(不要求对端在线)。
### 2.3 握手失败(relay 发错误帧后立即 Close)
```json
{ "kind": "control", "error": "<reason>" }
```
| `error` 值 | 触发条件 |
|---|---|
| `handshake_failed` | 10s 未发 Hello / Hello JSON 解析失败 / 首帧非文本 / Hello 超 1 MiB |
| `kind_mismatch` | Hello.kind 与端点不匹配(如 agent 端点收 commander) |
| `invalid_channel_id` | channel_id 为空 / >128B / 含控制字符 |
| `server_misconfigured` | 服务端 `U_RELAY_TOKEN` 未设置 |
| `auth_failed` | token 不匹配 |
### 2.4 已下线的旧值(一律拒绝)
| 维度 | 现行 | 已下线(收到即拒) |
|---|---|---|
| 执行端端点 | `/ws/agent` | `/ws/runner` `/ws/device` `/ws/executor` |
| 控制端端点 | `/ws/commander` | `/ws/console` `/ws/miniapp` `/ws/controller` |
| Hello.kind 执行端 | `"agent"` | `"runner"` `"device"` `"executor"` |
| Hello.kind 控制端 | `"commander"` | `"console"` `"miniapp"` `"controller"` |
| 配对键字段 | `channel_id` | `device_id`(不再 alias) |
| 错误码 | `invalid_channel_id` | `invalid_device_id` |
---
## 3. 数据帧(握手通过后)
### 3.1 客户端 → relay(入站)
入站文本帧即业务 payload,relay **不解析**直接透传:
```json
{ "任意 JSON" }
```
非 JSON 文本会被包成 `{"payload": "<原字符串>"}`。
**特殊语义**:若 payload 顶层有 `control_kind: "ping"`,触发 relay 本地心跳响应(见 §4),不透传对端。
### 3.2 relay → 客户端(出站 BroadcastMessage)
```json
{
"channel_id": "chan-xxx",
"kind": "event" | "command" | "control",
"source": 12345,
"from": "agent" | "commander",
"payload": { "原始 JSON" },
"ts": 1725000000000
}
```
| 字段 | 说明 |
|---|---|
| `channel_id` | 配对通道 |
| `kind` | `event`=agent→commander;`command`=commander→agent;`control`=控制面 |
| `source` | 来源 conn_id(u64,0=NIL 伪值,relay 自构造消息用) |
| `from` | 来源客户端类型 |
| `payload` | 原始 JSON 透传 |
| `ts` | relay 收到入站帧的毫秒时间戳 |
### 3.3 路由规则(relay 内部,客户端无需关心但需理解)
| 消息 | 投递目标 |
|---|---|
| `Event`(agent 发) | 同 channel_id 的**所有 commander**(除来源自身) |
| `Command`(commander 发) | 同 channel_id 的**所有 agent**(除来源自身) |
| `Control` | 同 channel_id 的**所有连接**(除来源自身) |
**方向硬约束**(relay 路由层强制):
- `Event` 永不投递给 agent
- `Command` 永不投递给 commander
**多端登录**:同 channel_id 可并存多个 agent 和多个 commander,全投递(Vec 索引)。
### 3.4 帧限制
| 限制 | 值 |
|---|---|
| 入站文本帧 | **≤ 1 MiB**(含 Hello),超限直接断开 |
| 二进制帧 | **忽略**(MVP 仅文本) |
| WS Ping/Pong | 协议层自动处理(tungstenite),不进业务流 |
---
## 4. 心跳协议
### 4.1 客户端发起
```json
{ "control_kind": "ping" }
```
**关键**:relay 收到后**本地直接回 pong**,不透传对端,也不依赖对端在线。
### 4.2 relay 响应
```json
{
"kind": "control",
"from": "agent",
"source": 0,
"payload": { "control_kind": "pong" },
"ts": 1725000000000
}
```
**字段细节**:
- `source: 0` 是 `ConnId::NIL` 伪值 —— 若填接收方自己的 conn_id,路由防回环会跳过,pong 永远回不来
- `from: "agent"` 是 relay 代理执行端身份 —— 让 commander 端识别为合法控制响应
### 4.3 客户端实现要点
| 端 | 用途 |
|---|---|
| **Commander** | 用 pong 更新 `lastPongTime`,看门狗超时则重连 |
| **Agent**(跑桌面/移动网络) | 用 pong 重置读死线,死线超时走既有重连 |
---
## 5. 部署配置
### 5.1 环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
| `U_RELAY_ADDR` | `0.0.0.0:8080` | 监听地址 |
| `U_RELAY_TOKEN` | **无(必填)** | 配对 token,未设置启动即 exit 1 |
| `RUST_LOG` | `info` | tracing env-filter |
### 5.2 nginx 反代参考
```nginx
location /ws/ {
proxy_pass http://127.0.0.1:9180;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
```
### 5.3 部署形态
- **musl 静态编译**:`x86_64-unknown-linux-musl` → 单二进制 ~3.6MB
- **无 openssl/native-tls**:TLS 完全由 nginx 终结
- **systemd 单元**:`/usr/local/bin/u-relay` + `/etc/u-relay.env`(权限 600)
---
## 6. 客户端实现 Checklist
### Agent(执行端,如 df-tunnel)
- [ ] 连 `wss://host/ws/agent`
- [ ] Hello `{kind:"agent", channel_id, token}`
- [ ] 等首条非 error 帧判定握手通过
- [ ] 心跳:周期发 `{"control_kind":"ping"}`,等 pong 重置读死线
- [ ] 入站 `BroadcastMessage`:仅处理 `kind=="command"`(来自 commander 的指令)和 `kind=="control"`
- [ ] 出站:业务事件 JSON 直接作为文本帧发送(relay 包成 `Event` 投递给 commander)
### Commander(控制端,如 df-miniapp)
- [ ] 连 `wss://host/ws/commander`
- [ ] Hello `{kind:"commander", channel_id, token}`
- [ ] 等首条非 error 帧判定握手通过
- [ ] 心跳:周期发 `{"control_kind":"ping"}`,等 pong 更新 `lastPongTime` 看门狗
- [ ] 入站 `BroadcastMessage`:仅处理 `kind=="event"`(来自 agent 的事件)和 `kind=="control"`
- [ ] 出站:业务指令 JSON 直接作为文本帧发送(relay 包成 `Command` 投递给 agent)
---
## 7. 离线语义
| 场景 | 行为 |
|---|---|
| 对端不在线时发消息 | relay **丢弃**(不缓存),debug 日志记录"广播无对端在线" |
| 客户端判断对端在线 | 通过 `PresenceState`(relay 不主动推送,由客户端心跳或业务层判活) |
| 重连 | 客户端指数退避自行实现(df-miniapp 已有 `reconnectBaseDelay`/`reconnectMaxDelay`) |
---
## 8. 安全说明
- **token 常量时间比较**:relay 端实现,防时序侧信道
- **channel_id 校验**:格式防御,无 per-channel token 绑定(Phase3 留)
- **风险**:单共享 token 下,持 token 者可声明任意 channel_id 冒充该通道 —— 生产部署需确保 token 不泄露
---
## 9. 与旧版 df-relay 协议差异速查
| 维度 | df-relay(已下线) | u-relay(现行) |
|---|---|---|
| 端点 | `/ws/device` `/ws/miniapp` | `/ws/agent` `/ws/commander` |
| Hello.kind | `"device"` `"miniapp"` | `"agent"` `"commander"` |
| 配对键 | `device_id` | `channel_id` |
| 心跳 pong | 由对端回(路径长) | relay 本地回(路径短) |
| 旧名兼容 | 多代别名 | **一律拒绝** |
| 错误码 | `invalid_device_id` | `invalid_channel_id` |
| 帧上限 | 无 | 1 MiB |
| token env | `DF_RELAY_TOKEN` | `U_RELAY_TOKEN` |