- 无人值守长任务设计定稿,含效率与可观测补遗 - u-relay wire 协议规格与五项评估结论 - 新能力引入评估纪律五项清单 - todo 销账与狗粮短板分析登记
8.4 KiB
8.4 KiB
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 内必发,否则断开)
{
"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 立即下发)
{ "kind": "control", "payload": { "control_kind": "hello_ack" } }
客户端判定:收到首条非 error 消息即握手通过(不要求对端在线)。
2.3 握手失败(relay 发错误帧后立即 Close)
{ "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 文本会被包成 {"payload": "<原字符串>"}。
特殊语义:若 payload 顶层有 control_kind: "ping",触发 relay 本地心跳响应(见 §4),不透传对端。
3.2 relay → 客户端(出站 BroadcastMessage)
{
"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永不投递给 agentCommand永不投递给 commander
多端登录:同 channel_id 可并存多个 agent 和多个 commander,全投递(Vec 索引)。
3.4 帧限制
| 限制 | 值 |
|---|---|
| 入站文本帧 | ≤ 1 MiB(含 Hello),超限直接断开 |
| 二进制帧 | 忽略(MVP 仅文本) |
| WS Ping/Pong | 协议层自动处理(tungstenite),不进业务流 |
4. 心跳协议
4.1 客户端发起
{ "control_kind": "ping" }
关键:relay 收到后本地直接回 pong,不透传对端,也不依赖对端在线。
4.2 relay 响应
{
"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 反代参考
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 |