diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index 9610640..844c080 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -54,6 +54,11 @@ pub struct TopicTokenStats { pub prompt_tokens: u64, pub completion_tokens: u64, pub total_tokens: u64, + /// 累计缓存命中的输入 tokens 数(老数据为 0)。 + /// 缺失此字段会导致 command 层序列化的 JSON 在此被静默丢弃(serde 忽略未知字段), + /// 前端缓存占比恒为 0——两结构体字段必须保持对齐。 + #[serde(default)] + pub cached_tokens: u64, #[serde(default, skip_serializing_if = "Option::is_none")] pub last_prompt_tokens: Option, #[serde(default)] @@ -357,3 +362,35 @@ pub fn serialize_inbound(msg: &WsInbound) -> Result { pub fn serialize_outbound(msg: &WsOutbound) -> Result { serde_json::to_string(msg) } + +#[cfg(test)] +mod tests { + use super::*; + + /// 回归测试:command 层 TopicTokenStats 序列化的 JSON 必须能无损还原到 + /// protocol 层同名结构体(websocket 适配器走这条 JSON 往返路径)。 + /// + /// 历史缺陷:protocol 版曾缺 cached_tokens 字段,serde 反序列化时静默丢弃, + /// 导致前端缓存占比恒为 0(后端解析、存库、聚合全部正常)。 + /// 两结构体字段必须保持对齐,此测试固化该不变量。 + #[test] + fn test_topic_token_stats_roundtrip_preserves_cached_tokens() { + let command_stats = crate::command::handlers::list_topics::TopicTokenStats { + prompt_tokens: 13916, + completion_tokens: 80, + total_tokens: 13996, + cached_tokens: 8832, + last_prompt_tokens: Some(6971), + context_window_tokens: 131072, + }; + + // 模拟 websocket 适配器的实际路径:command JSON → protocol 结构体 + let json = serde_json::to_string(&command_stats).unwrap(); + let protocol_stats: TopicTokenStats = serde_json::from_str(&json).unwrap(); + + assert_eq!(protocol_stats.cached_tokens, 8832); + assert_eq!(protocol_stats.prompt_tokens, 13916); + assert_eq!(protocol_stats.last_prompt_tokens, Some(6971)); + assert_eq!(protocol_stats.context_window_tokens, 131072); + } +}