优化消息里读/写内容显示样式
This commit is contained in:
@@ -574,6 +574,131 @@ impl ClaudeParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
"tool_use" => {
|
||||
// Top-level tool_use record (Claude Code JSONL format)
|
||||
let timestamp = parse_timestamp(&value).unwrap_or_else(Utc::now);
|
||||
let tool_name = value
|
||||
.get("tool_name")
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let input_preview = value.get("tool_input").map(|i| i.to_string());
|
||||
let synthetic_id = format!("tl-tool-{}", messages.len());
|
||||
|
||||
// Attach to last assistant message, or create a synthetic one
|
||||
if let Some(last) = messages
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|m| matches!(m.role, MessageRole::Assistant))
|
||||
{
|
||||
last.content.push(ContentBlock::ToolUse {
|
||||
tool_use_id: Some(synthetic_id),
|
||||
tool_name,
|
||||
input_preview,
|
||||
});
|
||||
} else {
|
||||
messages.push(UnifiedMessage {
|
||||
id: format!("synth-assistant-{}", messages.len()),
|
||||
role: MessageRole::Assistant,
|
||||
content: vec![ContentBlock::ToolUse {
|
||||
tool_use_id: Some(synthetic_id),
|
||||
tool_name,
|
||||
input_preview,
|
||||
}],
|
||||
timestamp,
|
||||
usage: None,
|
||||
duration_ms: None,
|
||||
model: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
"tool_result" => {
|
||||
// Top-level tool_result record (Claude Code JSONL format)
|
||||
let tool_output = value.get("tool_output");
|
||||
let tool_name = value
|
||||
.get("tool_name")
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("");
|
||||
let is_error = tool_output
|
||||
.and_then(|o| o.get("exit"))
|
||||
.and_then(|e| e.as_i64())
|
||||
.is_some_and(|code| code != 0);
|
||||
|
||||
// Extract output text: prefer "preview" (read), then "output" (bash)
|
||||
let output_text = tool_output
|
||||
.and_then(|o| {
|
||||
o.get("preview")
|
||||
.or_else(|| o.get("output"))
|
||||
.and_then(|v| v.as_str())
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// For read tools, structurize with start_line from tool_input.offset
|
||||
let output_preview =
|
||||
if tool_name == "read" || tool_name == "Read" {
|
||||
let start_line = value
|
||||
.get("tool_input")
|
||||
.and_then(|i| i.get("offset"))
|
||||
.and_then(|o| o.as_u64())
|
||||
.map(|o| o + 1)
|
||||
.unwrap_or(1);
|
||||
output_text.map(|text| {
|
||||
serde_json::json!({
|
||||
"start_line": start_line,
|
||||
"content": text
|
||||
})
|
||||
.to_string()
|
||||
})
|
||||
} else {
|
||||
output_text
|
||||
};
|
||||
|
||||
// Find matching ToolUse in the last assistant message and use its ID
|
||||
let matching_id = messages
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|m| matches!(m.role, MessageRole::Assistant))
|
||||
.and_then(|m| {
|
||||
m.content.iter().rev().find_map(|b| {
|
||||
if let ContentBlock::ToolUse {
|
||||
tool_use_id: Some(ref id),
|
||||
..
|
||||
} = b
|
||||
{
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Append ToolResult to the same assistant message so they stay in the same turn
|
||||
if let Some(last) = messages
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|m| matches!(m.role, MessageRole::Assistant))
|
||||
{
|
||||
last.content.push(ContentBlock::ToolResult {
|
||||
tool_use_id: matching_id,
|
||||
output_preview,
|
||||
is_error,
|
||||
});
|
||||
} else {
|
||||
messages.push(UnifiedMessage {
|
||||
id: format!("synth-result-{}", messages.len()),
|
||||
role: MessageRole::Assistant,
|
||||
content: vec![ContentBlock::ToolResult {
|
||||
tool_use_id: matching_id,
|
||||
output_preview,
|
||||
is_error,
|
||||
}],
|
||||
timestamp: parse_timestamp(&value).unwrap_or_else(Utc::now),
|
||||
usage: None,
|
||||
duration_ms: None,
|
||||
model: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -583,6 +708,7 @@ impl ClaudeParser {
|
||||
|
||||
let mut turns = group_into_turns(messages);
|
||||
super::relocate_orphaned_tool_results(&mut turns);
|
||||
super::structurize_read_tool_output(&mut turns);
|
||||
let context_window_used_tokens = latest_claude_context_window_used_tokens(&turns);
|
||||
let context_window_max_tokens =
|
||||
claude_context_window_max_tokens_for_model(model.as_deref());
|
||||
|
||||
@@ -773,6 +773,7 @@ impl CodexParser {
|
||||
|
||||
let mut turns = group_into_turns(messages);
|
||||
super::relocate_orphaned_tool_results(&mut turns);
|
||||
super::structurize_read_tool_output(&mut turns);
|
||||
let mut session_stats = super::compute_session_stats(&turns);
|
||||
session_stats =
|
||||
merge_codex_total_usage_stats(session_stats, latest_total_usage, latest_total_tokens);
|
||||
|
||||
@@ -556,6 +556,7 @@ impl GeminiParser {
|
||||
|
||||
let mut turns = group_into_turns(messages);
|
||||
super::relocate_orphaned_tool_results(&mut turns);
|
||||
super::structurize_read_tool_output(&mut turns);
|
||||
summary.message_count = turns.len() as u32;
|
||||
summary.id = conversation_id.to_string();
|
||||
let context_window_used_tokens = super::latest_turn_total_usage_tokens(&turns);
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod gemini;
|
||||
pub mod openclaw;
|
||||
pub mod opencode;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use regex::Regex;
|
||||
@@ -292,6 +292,117 @@ pub fn relocate_orphaned_tool_results(turns: &mut Vec<MessageTurn>) {
|
||||
turns.retain(|turn| !turn.blocks.is_empty());
|
||||
}
|
||||
|
||||
/// Convert Read tool output from numbered-line format to `{"start_line":N,"content":"..."}`.
|
||||
///
|
||||
/// Claude Code embeds line numbers in Read output like ` 115→content`.
|
||||
/// This splits on the `→` delimiter (or tab for older `cat -n` format),
|
||||
/// extracts the starting line number, and returns clean content.
|
||||
pub fn structurize_read_tool_output(turns: &mut [MessageTurn]) {
|
||||
let mut read_tool_ids: HashSet<String> = HashSet::new();
|
||||
for turn in turns.iter() {
|
||||
for block in &turn.blocks {
|
||||
if let ContentBlock::ToolUse {
|
||||
tool_use_id: Some(ref id),
|
||||
ref tool_name,
|
||||
..
|
||||
} = block
|
||||
{
|
||||
let name = tool_name.to_lowercase();
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"read" | "read_file" | "readfile" | "read file" | "cat" | "view"
|
||||
) {
|
||||
read_tool_ids.insert(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for turn in turns.iter_mut() {
|
||||
for block in turn.blocks.iter_mut() {
|
||||
let is_read_result = matches!(
|
||||
block,
|
||||
ContentBlock::ToolResult { tool_use_id: Some(ref id), .. }
|
||||
if read_tool_ids.contains(id)
|
||||
);
|
||||
if !is_read_result {
|
||||
continue;
|
||||
}
|
||||
if let ContentBlock::ToolResult {
|
||||
ref mut output_preview,
|
||||
..
|
||||
} = block
|
||||
{
|
||||
if let Some(ref text) = output_preview {
|
||||
if let Some(json) = strip_numbered_lines(text) {
|
||||
*output_preview = Some(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Known delimiters between line number and content.
|
||||
const LINE_NUM_DELIMITERS: &[&str] = &["→", "\t"];
|
||||
|
||||
/// Try to split a line at a known delimiter, returning (line_number, content).
|
||||
fn split_line_number(line: &str) -> Option<(u64, &str)> {
|
||||
for delim in LINE_NUM_DELIMITERS {
|
||||
if let Some(pos) = line.find(delim) {
|
||||
let prefix = line[..pos].trim();
|
||||
if let Ok(num) = prefix.parse::<u64>() {
|
||||
let content_start = pos + delim.len();
|
||||
return Some((num, &line[content_start..]));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// If most lines have a recognized line-number prefix, strip them all
|
||||
/// and return `{"start_line":N,"content":"clean text"}`.
|
||||
fn strip_numbered_lines(text: &str) -> Option<String> {
|
||||
let raw_lines: Vec<&str> = text.lines().collect();
|
||||
if raw_lines.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let matched = raw_lines
|
||||
.iter()
|
||||
.filter(|l| l.is_empty() || split_line_number(l).is_some())
|
||||
.count();
|
||||
if matched < raw_lines.len() * 4 / 5 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut start_line: u64 = 1;
|
||||
let mut first = true;
|
||||
let stripped: Vec<&str> = raw_lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
if let Some((num, content)) = split_line_number(line) {
|
||||
if first {
|
||||
start_line = num;
|
||||
first = false;
|
||||
}
|
||||
content
|
||||
} else {
|
||||
first = false;
|
||||
*line
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(
|
||||
serde_json::json!({
|
||||
"start_line": start_line,
|
||||
"content": stripped.join("\n")
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract the last path component as the folder name.
|
||||
pub fn folder_name_from_path(path: &str) -> String {
|
||||
path.rsplit(['/', '\\']).next().unwrap_or(path).to_string()
|
||||
|
||||
@@ -652,6 +652,7 @@ impl OpenClawParser {
|
||||
let folder_name = folder_path.as_ref().map(|p| folder_name_from_path(p));
|
||||
let mut turns = group_into_turns(messages);
|
||||
super::relocate_orphaned_tool_results(&mut turns);
|
||||
super::structurize_read_tool_output(&mut turns);
|
||||
|
||||
let context_window_used_tokens = latest_turn_total_usage_tokens(&turns);
|
||||
let context_window_max_tokens = session_meta
|
||||
@@ -983,9 +984,15 @@ fn extract_assistant_content(value: &serde_json::Value) -> Vec<ContentBlock> {
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let is_edit_tool = matches!(
|
||||
tool_name.to_lowercase().as_str(),
|
||||
"edit" | "write" | "apply_patch" | "patch" | "applypatch"
|
||||
| "edit_file" | "editfile"
|
||||
);
|
||||
let max_len = if is_edit_tool { 50000 } else { 500 };
|
||||
let input_preview = item.get("arguments").map(|a| {
|
||||
let s = a.to_string();
|
||||
truncate_str(&s, 500)
|
||||
truncate_str(&s, max_len)
|
||||
});
|
||||
blocks.push(ContentBlock::ToolUse {
|
||||
tool_use_id,
|
||||
|
||||
@@ -189,6 +189,7 @@ impl OpenCodeParser {
|
||||
let messages = self.load_sqlite_messages(&conn, conversation_id).await?;
|
||||
let mut turns = group_into_turns(messages);
|
||||
super::relocate_orphaned_tool_results(&mut turns);
|
||||
super::structurize_read_tool_output(&mut turns);
|
||||
let context_window_used_tokens = super::latest_turn_total_usage_tokens(&turns);
|
||||
let context_window_max_tokens =
|
||||
super::infer_context_window_max_tokens(summary.model.as_deref());
|
||||
|
||||
Reference in New Issue
Block a user