优化上下文事件更新的处理逻辑

This commit is contained in:
xintaofei
2026-03-13 22:07:29 +08:00
parent bf14a99168
commit 0c843ec14e
3 changed files with 131 additions and 2 deletions

View File

@@ -58,6 +58,19 @@ fn is_meta_message(value: &serde_json::Value) -> bool {
.unwrap_or(false)
}
/// Check if an assistant message is a synthetic placeholder (e.g. generated by
/// Claude Code for local commands like `/context` or `/model`).
/// These carry `model: "<synthetic>"` and all-zero usage, so they should be
/// excluded from conversation turns and stats.
fn is_synthetic_assistant(value: &serde_json::Value) -> bool {
value
.get("message")
.and_then(|m| m.get("model"))
.and_then(|m| m.as_str())
.map(|s| s == "<synthetic>")
.unwrap_or(false)
}
fn parse_model_capacity_suffix(model: &str) -> Option<u64> {
let captures = model_capacity_suffix_regex().captures(model.trim())?;
let value = captures.get(1)?.as_str().parse::<f64>().ok()?;
@@ -236,6 +249,11 @@ impl ClaudeParser {
}
if msg_type == "user" || msg_type == "assistant" {
// Skip synthetic assistant placeholders for local commands
if msg_type == "assistant" && is_synthetic_assistant(&value) {
continue;
}
message_count += 1;
// Extract model from assistant messages
@@ -449,6 +467,10 @@ impl ClaudeParser {
}
match msg_type {
"assistant" if is_synthetic_assistant(&value) => {
// Skip synthetic assistant placeholders for local commands
continue;
}
"user" => {
let content = extract_user_content(&value);
@@ -1036,6 +1058,103 @@ mod tests {
assert_eq!(resolved, PathBuf::from("/Users/default/.claude"));
}
#[test]
fn synthetic_assistant_excluded_from_detail() {
let path = std::env::temp_dir().join(format!(
"codeg-claude-synthetic-{}.jsonl",
uuid::Uuid::new_v4()
));
let mut file = fs::File::create(&path).expect("create temp jsonl");
// Normal user message
writeln!(
file,
"{}",
json!({
"type": "user",
"sessionId": "synth-test",
"timestamp": "2026-03-01T10:00:00Z",
"uuid": "u1",
"cwd": "/tmp/demo",
"message": {
"content": [{"type": "text", "text": "hello"}]
}
})
)
.unwrap();
// Normal assistant message with real usage
writeln!(
file,
"{}",
json!({
"type": "assistant",
"sessionId": "synth-test",
"timestamp": "2026-03-01T10:00:02Z",
"uuid": "a1",
"message": {
"model": "claude-sonnet-4-6",
"content": [{"type": "text", "text": "world"}],
"usage": {
"input_tokens": 1000,
"output_tokens": 200,
"cache_creation_input_tokens": 300,
"cache_read_input_tokens": 400
}
}
})
)
.unwrap();
// Synthetic assistant from a local command like /context
writeln!(
file,
"{}",
json!({
"type": "assistant",
"sessionId": "synth-test",
"timestamp": "2026-03-01T10:01:00Z",
"uuid": "a2",
"message": {
"model": "<synthetic>",
"content": [{"type": "text", "text": "No response requested."}],
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
})
)
.unwrap();
let parser = ClaudeParser {
base_dir: PathBuf::new(),
};
let detail = parser
.parse_conversation_detail(&path, "synth-test")
.expect("parse detail");
fs::remove_file(&path).unwrap();
// Should have 2 turns (user + real assistant), synthetic is excluded
assert_eq!(detail.turns.len(), 2);
assert!(
!detail
.turns
.iter()
.any(|t| t.blocks.iter().any(|b| matches!(
b,
ContentBlock::Text { text } if text == "No response requested."
))),
"synthetic assistant content should not appear in turns"
);
// Stats should reflect only the real assistant usage
let stats = detail.session_stats.expect("session stats");
assert_eq!(stats.context_window_used_tokens, Some(1700));
assert_eq!(stats.context_window_max_tokens, Some(200_000));
let total = stats.total_tokens.expect("total tokens");
assert_eq!(total, 1900); // 1000 + 200 + 300 + 400
}
#[test]
fn extract_user_content_parses_claude_base64_image_block() {
let value = json!({