Files
DevFlow/crates/df-tunnel/src/events.rs
T
lxy b37279f3a2 新增: 待发箱/唤醒原语/预算随窗口 + 治理门禁与场景护栏
- 待发箱地基: 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 测试
2026-10-10 08:09:17 +08:00

199 lines
7.3 KiB
Rust

//! df-tunnel 隧道事件类型
//!
//! 设计依据:设计文档「三、技术选型」指出 AiChatEvent 17 变体在两端同语言保证类型一致。
//! 但 AiChatEvent 定义在 src-tauri 非 crate 层,跨 crate path 引用会破坏分层边界
//! (lib crate 依赖 src-tauri 二进制不合理)。故此处**独立定义隧道透传子集**:
//! - 仅保留跨端高频透传的 4 个变体(TextDelta/ToolCall/Approval/Completed),
//! Phase2 可按需追加,不依赖 src-tauri。
//! - 携带 conv_id 字段做多会话路由(对齐 F-09 多会话 conv_id 契约)。
//!
//! 向后兼容:新增变体只加不减,旧消费方忽略未知变体(serde 配 `#[serde(other)]` 捕获兜底)。
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 与本类型。
///
/// 历史:df-relay 一代协议用 device_id + kind:"device",已在 u-relay 现行版全部下线
/// (端点 /ws/agent,kind:"agent",配对键 channel_id,旧名握手期直接拒绝)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hello {
/// 客户端类型("agent" / "commander"),桌面端恒为 Agent
pub kind: HelloKind,
/// 配对绑定的通道 ID(非空,≤128B,无控制字符)
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 一致)
///
/// 现行仅两值:Agent(执行端,如 df-tunnel)/ Commander(控制端,如 df-miniapp)。
/// 历史名 Device/Miniapp/Runner/Console/Executor/Controller 已全部下线。
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HelloKind {
Agent,
Commander,
}
/// relay 控制面错误帧(握手失败时 relay 发送)
///
/// relay 在校验失败/连接异常时发 `{"kind":"control","error":"<reason>"}` 文本帧。
/// 桌面端据此识别握手失败原因(不依赖 error 帧亦可:连接被 Close 即视为失败)。
///
/// 现行 u-relay 错误码:handshake_failed / kind_mismatch / invalid_channel_id /
/// server_misconfigured / auth_failed。历史 `invalid_device_id` 已下线。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelayControlError {
/// 固定 "control"(用于与正常 BroadcastMessage 区分,relay 错误帧简化为两字段)
pub kind: String,
/// 错误原因(handshake_failed / kind_mismatch / invalid_channel_id /
/// server_misconfigured / auth_failed)
pub error: String,
}
/// 隧道透传的事件类型(AiChatEvent 简化子集)
///
/// 变体语义对齐桌面端 ai-chat-event:
/// - `TextDelta`:流式增量文本块
/// - `ToolCall`:工具调用请求(含工具名/参数)
/// - `Approval`:审批请求(用户授权确认)
/// - `Completed`:单轮对话完成
///
/// Phase2 可扩展 Error/Stopped/Regenerated 等变体。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TunnelEvent {
/// 流式文本增量
TextDelta {
/// 会话 ID(多会话路由用,对齐 F-09)
conv_id: String,
/// 增量文本片段
delta: String,
},
/// 工具调用请求
ToolCall {
/// 会话 ID
conv_id: String,
/// 工具调用唯一标识(用于回填结果)
call_id: String,
/// 工具名
name: String,
/// 工具参数(原始 JSON,由消费方按 name 解析)
arguments: serde_json::Value,
},
/// 审批请求(工具/操作需用户授权)
Approval {
/// 会话 ID
conv_id: String,
/// 审批项唯一标识
approval_id: String,
/// 审批描述(展示给用户)
description: String,
},
/// 对话单轮完成
Completed {
/// 会话 ID
conv_id: String,
/// 本轮产生的消息 ID
message_id: String,
},
}
impl TunnelEvent {
/// 取事件所属会话 ID(路由分发的便利方法)
pub fn conv_id(&self) -> &str {
match self {
TunnelEvent::TextDelta { conv_id, .. }
| TunnelEvent::ToolCall { conv_id, .. }
| TunnelEvent::Approval { conv_id, .. }
| TunnelEvent::Completed { conv_id, .. } => conv_id,
}
}
}
/// 云后端 → 桌面端的操作指令(控制端远程操作映射)
///
/// 对齐设计文档「Layer1 指令路由」:
/// send/stop/approve/regenerate/switch 五类操作。
/// 由控制端(df-miniapp 等 Commander)发出,经 u-relay 路由到本桌面端 Agent。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TunnelCommand {
/// 发送消息
Send { conv_id: String, content: String },
/// 停止当前生成
Stop { conv_id: String },
/// 审批通过/拒绝(approval_id + accepted)
Approve { conv_id: String, approval_id: String, accepted: bool },
/// 重新生成
Regenerate { conv_id: String },
/// 切换对话
Switch { conv_id: String },
}
/// 隧道传输的消息包装(双向)
///
/// 区分「桌面端 → u-relay → 控制端」的事件流(TunnelEvent)
/// 与「控制端 → u-relay → 桌面端」的指令流(TunnelCommand)。
/// 同一 WS 连接复用,靠 kind 标签区分。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "dir", rename_all = "snake_case")]
pub enum TunnelMessage {
/// 桌面端发出的事件(下行)
Event(TunnelEvent),
/// 云后端转发的指令(上行,发往桌面端)
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);
}
}