新增: 待发箱/唤醒原语/预算随窗口 + 治理门禁与场景护栏
- 待发箱地基: V51 ai_outbox 表 + 手写 OutboxRepo(claim CAS/崩溃残留复位/瞬时失败保留) + outbox.rs 调度器(KV 默认关/Notify 唤醒/会话忙跳过不丢)
- Wake 异步唤醒原语: 三条件(delay/file-appear/process-exit)+纯判定+注入走正典 spawn_agentic_session,默认关
- 上下文预算随模型窗口: model_context.rs 三级优先(池显式>预设表>回退 128K);上界取 1M 而非 128K——max_tokens 仅裁剪预算,卡 128K 等于不治根因
- 文件租约补纯函数与单测(未另造双轨:LEASE 早随 cb260ab 落地) / 跨会话召回补纯函数单测(主体早已落地)
- 子代理: 完整卡已消化 + 子会话压缩层 + P3 批量
- 隧道/中继: 指令离线不丢 + Hello proto 版本 + 心跳与 runbook
- df-mcp: 写工具后落 KV 变更标记(供 GUI 轮询兜底,补独立 stdio 进程无感知)
- 迁移链顶点改单一真值常量 LATEST_SCHEMA_VERSION(修 2 处因 V51 失效的断言)
- 治理: i18n 特殊字符提交门禁(含反引号覆盖 + 10 自测) / 根目录卫生门禁 / 看板归档器 / 单一 lockfile(bun) / swap-install 落 temp/ 且兼容无 Get-FileHash
- 场景护栏: 流式正文可见性 e2e(playwright 免真机事件回放) + 列表续写/外置句柄 node 测试
This commit is contained in:
@@ -11,12 +11,25 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 当前 Hello 握手协议版本。
|
||||
///
|
||||
/// 协商规则:旧客户端 Hello 缺 `proto` 字段 → 视为 v1(向后兼容);
|
||||
/// 高于对端支持版本时由 peer 记录告警,为后续破坏性变更预留通知位。
|
||||
pub const HELLO_PROTO_VERSION: u32 = 1;
|
||||
|
||||
/// `proto` 缺省值:旧客户端不带该字段时按 v1 处理(向后兼容)。
|
||||
fn default_proto() -> u32 {
|
||||
HELLO_PROTO_VERSION
|
||||
}
|
||||
|
||||
/// 握手身份宣告帧(对齐 u-relay::relay::Hello)
|
||||
///
|
||||
/// 桌面端 WS 连 `/ws/agent` 后,首条消息发此 JSON,relay 校验 kind 与 token。
|
||||
/// 字段名/serde rename 与 u-relay 严格一致(`#[serde(rename_all="snake_case")]`),
|
||||
/// 否则 relay 反序列化失败 → 握手被拒。
|
||||
///
|
||||
/// `proto` 为协议版本,供 relay 端做版本协商(缺字段按 v1,新增破坏性变更时递增)。
|
||||
///
|
||||
/// 注意:本 crate 不依赖 u-relay(避免 lib→server 反向依赖),自行镜像协议结构。
|
||||
/// 协议字段如有变更须同步 u-relay::relay::Hello 与本类型。
|
||||
///
|
||||
@@ -30,6 +43,9 @@ pub struct Hello {
|
||||
pub channel_id: String,
|
||||
/// 配对 token(env U_RELAY_TOKEN 同源)
|
||||
pub token: String,
|
||||
/// 协议版本(缺省按 v1,兼容旧客户端)
|
||||
#[serde(default = "default_proto")]
|
||||
pub proto: u32,
|
||||
}
|
||||
|
||||
/// Hello.kind 传输表示(serde 字符串,与 u-relay::ClientKindWire 一致)
|
||||
@@ -155,3 +171,28 @@ pub enum TunnelMessage {
|
||||
/// 云后端转发的指令(上行,发往桌面端)
|
||||
Command(TunnelCommand),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hello_proto_defaults_to_v1_when_absent() {
|
||||
// 旧客户端不发 proto → 反序列化按 v1,保证向后兼容
|
||||
let json = r#"{"kind":"agent","channel_id":"c1","token":"t"}"#;
|
||||
let h: Hello = serde_json::from_str(json).expect("缺 proto 应可解析");
|
||||
assert_eq!(h.proto, HELLO_PROTO_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_serializes_proto_field() {
|
||||
let h = Hello {
|
||||
kind: HelloKind::Agent,
|
||||
channel_id: "c1".into(),
|
||||
token: "t".into(),
|
||||
proto: HELLO_PROTO_VERSION,
|
||||
};
|
||||
let v: serde_json::Value = serde_json::to_value(&h).unwrap();
|
||||
assert_eq!(v["proto"], HELLO_PROTO_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ use tokio_tungstenite::tungstenite::protocol::CloseFrame;
|
||||
use tokio_tungstenite::tungstenite::Message as WsMessage;
|
||||
|
||||
use crate::error::{Result, TunnelError};
|
||||
use crate::events::{Hello, HelloKind, RelayControlError, TunnelEvent};
|
||||
use crate::events::{Hello, HelloKind, RelayControlError, TunnelEvent, HELLO_PROTO_VERSION};
|
||||
|
||||
/// 收到 commander→agent 指令 payload 时的回调类型(Boxed Future,在收发循环 task 内 await)
|
||||
///
|
||||
@@ -105,9 +105,13 @@ const HANDSHAKE_PROBE: Duration = Duration::from_secs(3);
|
||||
/// 包 `tokio::time::timeout` 在此窗口内未完成 → 返 TunnelError::Connect 显式错误。
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
/// 心跳间隔(应用层 Ping,补协议层 keepalive,防 NAT 连接表超时回收)
|
||||
/// u-relay 心跳走应用层 control 帧 ping→pong,relay 本地直回不转对端
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(25);
|
||||
/// 心跳间隔(WS 协议层 Ping,补 TCP keepalive,防 NAT/反代空闲回收连接)。
|
||||
///
|
||||
/// 上界约束:链路最短空闲超时在反代侧(生产 nginx `proxy_read_timeout`,配置样例见
|
||||
/// u-relay wire 协议文档 §5.2,最小 300s),间隔须显著小于它并留余量——容忍数次心跳
|
||||
/// 丢失或网络抖动仍不被判定空闲断链。取 30s:满足约束,且与 commander 参考实现
|
||||
/// (u-ppt 30s ping)节拍一致,避免两端看门狗口径不一导致误重连。
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// 重连初始退避
|
||||
const RECONNECT_BASE: Duration = Duration::from_secs(1);
|
||||
@@ -201,7 +205,8 @@ impl TunnelClient for WsTunnelClient {
|
||||
)));
|
||||
}
|
||||
};
|
||||
tracing::info!(%url, "WS 连接已建立,开始 Hello 握手");
|
||||
// 版本协商(本端宣告):Hello 携带 proto,供 relay 侧比对决定兼容性
|
||||
tracing::info!(%url, proto = HELLO_PROTO_VERSION, "WS 连接已建立,开始 Hello 握手");
|
||||
|
||||
let (mut sink, mut stream) = ws_stream.split();
|
||||
|
||||
@@ -210,6 +215,7 @@ impl TunnelClient for WsTunnelClient {
|
||||
kind: HelloKind::Agent,
|
||||
channel_id: channel_id.to_string(),
|
||||
token: token.to_string(),
|
||||
proto: HELLO_PROTO_VERSION,
|
||||
};
|
||||
let hello_json = serde_json::to_string(&hello)?;
|
||||
sink.send(WsMessage::Text(hello_json)).await.map_err(|e| {
|
||||
|
||||
Reference in New Issue
Block a user