From ca7dcabbdefa3c6698d0cd48773dc2d3796d2443 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Sat, 15 Aug 2026 21:02:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(protocol):=20TopicTokenStats=20=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20cached=5Ftokens=20=E5=AD=97=E6=AE=B5=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF=E7=BC=93=E5=AD=98=E5=8D=A0?= =?UTF-8?q?=E6=AF=94=E6=81=92=E4=B8=BA=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:command 层与 protocol 层存在两个同名 TopicTokenStats 结构体,websocket 适配器做 JSON 往返时 protocol 版缺 cached_tokens 字段,serde 反序列化静默丢弃。后端解析/存库/聚合均正常(DB 实测 8832/13916 命中),仅传输层断链。补字段并加往返回归测试固化两结构体对齐不变量。 --- src/protocol/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) 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); + } +}