- 看板: 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·简洁·扩展性·高内聚低耦合·单一真值来源)
273 lines
9.6 KiB
Markdown
273 lines
9.6 KiB
Markdown
# 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` |
|