# u-relay 运维手册(token 轮换 · 一通道一 agent · 心跳约束) > 适用:u-relay 跨端中继的日常运维纪律。协议细节见 > [u-relay Wire 协议规格](./u-relay-wire-protocol-2026-10-04.md),本手册只讲**运维动作与约束**。 > 来源:协议评估遗留三项(单共享 token 可冒充 / 心跳间隔未定值 / 同 channel 多 agent 全投递)。 > 本手册**不含**任何 token 值、主机地址等敏感信息——真值只在部署机 `/etc/u-relay.env` 与各客户端本地配置里。 --- ## 1. 心跳间隔约束 心跳间隔须显著小于**链路最短空闲超时**(反代 `proxy_read_timeout`,配置样例 300s;生产实时值可能更大), 以容忍连续数次心跳丢失仍不断链。**约定 30–60s**,各方实现见协议规格 §4.4: - Agent(df-tunnel):`HEARTBEAT_INTERVAL` 常量,30s(WS 协议层 Ping) - Commander 参考(u-ppt):30s(应用层 `control_kind:"ping"`) > 改反代 `proxy_read_timeout` 时须同步评估心跳间隔:若把超时压到 300s 以下,间隔也要相应收窄并保持「超时 / 间隔 ≥ 5」量级的余量。 --- ## 2. 单共享 token 轮换 Runbook ### 2.1 现状与风险 u-relay 鉴权是**单一共享 token**(env `U_RELAY_TOKEN`,服务端常量时间比较),**不做 per-channel 隔离**: 任何持有该 token 的客户端可声明**任意 `channel_id`** 冒充任一通道。故 token 属敏感凭据, 泄露即等于整条中继失守;轮换是唯一的止损手段。 ### 2.2 涉及方与各自持有位置 token 值散落在「服务端 + 多个客户端」,**没有集中下发通道**: | 方 | 角色 | token 持有位置 | |---|---|---| | u-relay 服务端 | 鉴权源 | 部署机 `/etc/u-relay.env`(`U_RELAY_TOKEN`,权限 600)或 systemd unit 的 `Environment=` | | devflow 桌面端 | Agent | `relay_token` 设置项(运行时读,缺省回退硬编码默认值) | | devflow 小程序 | Commander | `apps/df-miniapp/src/config.ts` 缺省 + 设置页可改 | | devflow ask_uclaw | Commander | 设置页 `uclaw_token`(与 relay token 同源) | | u-claw | Agent/Runner | u-claw 侧配置 | | u-ppt | Commander | `relay.ts` 配置 | > 新增任何接入端都要登记到本表——漏一端的后果见下节。 ### 2.3 锁步成本(务必先读) 因为**服务端只认一个 token、客户端各自缓存**,轮换天然是一个**锁步切换**: - **无重叠窗口**:服务端不能同时接受新旧两个 token,故不存在「灰度换端」的过渡期。 - **顺序无解**:先改服务端 → 全部旧客户端立即 `auth_failed` 掉线;先改客户端 → 全部客户端在服务端切换前拿新 token 一样 `auth_failed`。两条路都有中断窗口。 - **漏端即静默失联**:任一客户端未同步换 token,会持续 `auth_failed` 被拒连,表现为**该端静默离线**(无业务错误、仅 relay 日志有 `auth_failed`),排查成本高。 - **不可逐端滚动**:无法像多 token 系统那样「先切一半、观察、再切一半」。 结论:**轮换是一次计划内的协调停机**,中断窗口 = 从「首个客户端改值」到「服务端重启加载新值」的间隔。执行要点是**把客户端改值的动作压到最短、紧接着重启服务端**。 ### 2.4 执行步骤 前置:准备好新 token,确认上表**每一端**都有人能改、改完可验证。 1. **写服务端新值(先不重启)**:在部署机把 `/etc/u-relay.env`(或 unit `Environment=`)的 `U_RELAY_TOKEN` 改为新值,保持服务仍在跑旧值。此时新值尚未生效。 2. **集中改用各客户端**:将上表每一端改到新值。此步开始后各端会 `auth_failed`,**中断窗口从这里起算**。 3. **重启服务端加载新值**:`systemctl restart u-relay`。重启后已改端的客户端重连即通。 4. **验证**:见 2.5。 5. **未改端收尾**:对仍 `auth_failed` 的端补改(此时只有漏改者受影响,不再影响大局)。 > 若某些端的 token 是**编译期硬编码**(无运行时配置项),须提前把「新 token + 重新打包」的产物备好,否则第 2 步无法即时完成,窗口会被拉长。 ### 2.5 验证 - 服务端:`systemctl status u-relay` 正常,`ss -tlnp | grep 9180` 在听。 - 连接:拿新 token 走一次握手冒烟(`/ws/agent` 或 `/ws/commander`,收 `hello_ack` 即通)。 - 日志:确认无 `auth_failed` 残留——有则说明还有端没换到新 token。 ### 2.6 回滚 轮换失败(如某端无法改)时,**把服务端 env 回退到旧 token 并再重启一次**,回到轮换前状态;客户端已改的端随之 `auth_failed`,需一并回退。回滚与轮换同样是锁步中断。 ### 2.7 根治方向 根治是**去掉单共享 token**:服务端改为 per-channel / per-client token(客户端拿自己的凭据只能绑自己的 channel), 届时任一端轮换互不影响,锁步成本消失。在此之前,本 runbook 的锁步纪律是必须遵守的现状约束。 --- ## 3. 运维纪律:一通道一 agent ### 3.1 现象与根因 u-relay 路由按 `channel_id` 配对,且**同 channel_id 允许多个 agent 并存、命令全投递**(Vec 索引,无去重、无选举)。 即:同 `channel_id` 上跑两个 agent 实例(如两个 devflow 桌面端、或 devflow + u-claw runner 用了同一通道), 一条 commander 指令会被**两端各执行一次**——写文件、跑命令、调外部接口全部重复,且两端事件回传互相交错。 ### 3.2 危害 - 指令重复执行:副作用翻倍(重复落库 / 重复发消息 / 重复改文件),且难以察觉。 - 事件串台:commander 收到两份进度/结果,流式 UI 错乱;两 agent 的上下文互相污染。 - 隐性化:两端都「看起来正常工作」,问题只在副作用层面暴露,事后才追溯得到。 ### 3.3 纪律 - **一通道一 agent**:每个 `channel_id` 在任一时刻**只允许一个 active agent**。通道不是「实例池」。 - **启动前先查**:起第二个 agent 前,确认目标 `channel_id` 上没有在跑的 agent(换一个独立 `channel_id`,或用不同通道前缀)。 - **换装/重启要收尾**:旧 agent 若未干净退出(进程被杀但连接残留),WS 连接可能存活到对端超时,期间新旧两连接同通道并存 = 短暂双投递。换装脚本须确认旧连接已断、或等旧连接超时后再起新实例。 - **命名隔离**:不同用途用不同 `channel_id`(如按机器 / 按项目 / 按用户拆通道),从命名上杜绝复用。 ### 3.4 排查手法 怀疑重复执行时: - 看 relay 日志:同一 `channel_id` 是否同时有 >1 条 agent 连接。 - 看 commander 侧:同一 `request_id` 是否收到两份 `result` / 事件重复。 - 看副作用:文件 mtime、DB 记录、外部调用日志是否成对出现。 确认后,断开多余 agent 连接(停掉重复实例)即可恢复单投递。 --- ## 附:关联 - 协议全貌:[u-relay Wire 协议规格](./u-relay-wire-protocol-2026-10-04.md)(§4 心跳、§5 部署、§3.3 路由) - u-relay 仓 README / 部署技能:中继二进制与 systemd 发布流程 - 看板登记:`docs/todo.md`「新能力引入评估纪律」下三项 P3