Files
DevFlow/docs/02-架构设计/专项设计/u-relay-wire-protocol-2026-10-04.md
lxy 8df4e33656 文档: 看板销账与登记 + 设计档六份 + 索引补录
- 看板: 907 补注/909~941 登记与销账(含 929 模型停用根因与 4 处次生缺口、931 队列丢失、933/935/937 新需求、939 上下文瓶颈清单、941 C-r0 行为变更待拍板)
- 归档: 全量完成态条目/段落 + 只读核验销账 4 段迁 todo归档/2026-10-10.md(机械搬运零措辞改动,未完成项零丢失)
- 设计档: 输入框列表预览与自动编号 / 定时消息与待发箱持久化(统一 ai_outbox 底座) / 对话内定时延时任务 / 消息渲染管线重构(P0-P2 分期) / 上下文分层组装器落地方案(记账基点单一真值,与并行写者职责划清) / FileTree 大目录方案(推荐后端分页,否决虚拟滚动) / u-relay 运维手册
- 索引与事实修正: docs/INDEX crate 数 8→13 + 四子树补录 25 条 + 专项设计补录 27 条;纠正「项目无独立 ARCHITECTURE.md」等过时声明;统一 bun 口径
- CLAUDE.md: 新增「命名与代码风格」节(变量/方法名精简+准确;DRY·简洁·扩展性·高内聚低耦合·单一真值来源)
2026-10-10 08:09:25 +08:00

273 lines
9.6 KiB
Markdown
Raw Permalink 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 重置读死线,死线超时走既有重连 |
### 4.4 心跳间隔约束
心跳间隔不是自由值,**链路各层空闲超时中最小的一层决定其上界**:
| 层 | 空闲超时 | 说明 |
|---|---|---|
| nginx 反代 | `proxy_read_timeout`(样例 300s;生产实时配置可能更大) | 超时即静默断开 UPSTREAM WS,无任何错误帧 |
| NAT / 防火墙 | 数十秒 ~ 数分钟(设备相关) | 空闲连接被回收,后续帧永久不可达 |
**约定**:间隔须**显著小于最短超时**并留足余量(容忍连续数次心跳丢失 / 网络抖动仍不断链),**推荐 30–60s**。低于 30s 徒增无效流量,超过 60s 在 300s 反代下余量偏薄。
| 端 | 保活帧 | 间隔 |
|---|---|---|
| Commander 参考(u-ppt) | 应用层 `{control_kind:"ping"}` | 30s |
| Agent(df-tunnel) | WS 协议层 Ping 帧(对端协议栈自动回 Pong) | 30s |
> Agent 侧用协议层 Ping 即可保活——WS Ping/Pong 由对端协议栈(tungstenite/axum)自动应答,同样刷新反代空闲计时;应用层 `control_kind:"ping"` 主要用于需要显式 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` |