mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
bd3cfb7741
* fix: parse tool_use/tool_result messages and add OpenCode SQLite backend - Claude: reclassify user messages containing tool_result as "tool" role - Codex: handle function_call and function_call_output payload types - Gemini: support array content and toolCalls extraction, filter info/error types - OpenCode: add SQLite session scan, load and delete alongside legacy JSON - utils: extend parse_timestamp_to_ms for integer timestamps, extract tool_use/tool_result in shared extract_text * fix: address remaining issues from tool_use/tool_result parsing commit - Claude: fix role misclassification for mixed user+tool_result messages (any → all) - OpenCode: extract duplicate part text logic into extract_part_text() - OpenCode: add path validation for SQLite delete to prevent foreign DB access - OpenCode: wrap SQLite deletion in transaction for atomicity - openclaw_config: remove redundant as_deref() on Option<&str>
175 lines
5.2 KiB
Rust
175 lines
5.2 KiB
Rust
use std::fs::File;
|
|
use std::io::{self, BufRead, BufReader, Seek, SeekFrom};
|
|
use std::path::Path;
|
|
|
|
use chrono::{DateTime, FixedOffset};
|
|
use serde_json::Value;
|
|
|
|
/// Read the first `head_n` lines and last `tail_n` lines from a file.
|
|
/// For small files (< 16 KB), reads all lines once to avoid unnecessary seeking.
|
|
pub fn read_head_tail_lines(
|
|
path: &Path,
|
|
head_n: usize,
|
|
tail_n: usize,
|
|
) -> io::Result<(Vec<String>, Vec<String>)> {
|
|
let file = File::open(path)?;
|
|
let file_len = file.metadata()?.len();
|
|
|
|
// For small files, read all lines once and split
|
|
if file_len < 16_384 {
|
|
let reader = BufReader::new(file);
|
|
let all: Vec<String> = reader.lines().map_while(Result::ok).collect();
|
|
let head = all.iter().take(head_n).cloned().collect();
|
|
let skip = all.len().saturating_sub(tail_n);
|
|
let tail = all.into_iter().skip(skip).collect();
|
|
return Ok((head, tail));
|
|
}
|
|
|
|
// Read head lines from the beginning
|
|
let reader = BufReader::new(file);
|
|
let head: Vec<String> = reader.lines().take(head_n).map_while(Result::ok).collect();
|
|
|
|
// Seek to last ~16 KB for tail lines
|
|
let seek_pos = file_len.saturating_sub(16_384);
|
|
let mut file2 = File::open(path)?;
|
|
file2.seek(SeekFrom::Start(seek_pos))?;
|
|
let tail_reader = BufReader::new(file2);
|
|
let all_tail: Vec<String> = tail_reader.lines().map_while(Result::ok).collect();
|
|
|
|
// Skip first partial line if we seeked into the middle of a line
|
|
let skip_first = if seek_pos > 0 { 1 } else { 0 };
|
|
let usable: Vec<String> = all_tail.into_iter().skip(skip_first).collect();
|
|
let skip = usable.len().saturating_sub(tail_n);
|
|
let tail = usable.into_iter().skip(skip).collect();
|
|
|
|
Ok((head, tail))
|
|
}
|
|
|
|
pub fn parse_timestamp_to_ms(value: &Value) -> Option<i64> {
|
|
// Integer: milliseconds (>1e12) or seconds
|
|
if let Some(n) = value.as_i64() {
|
|
return Some(if n > 1_000_000_000_000 { n } else { n * 1000 });
|
|
}
|
|
if let Some(n) = value.as_f64() {
|
|
let n = n as i64;
|
|
return Some(if n > 1_000_000_000_000 { n } else { n * 1000 });
|
|
}
|
|
// RFC3339 string
|
|
let raw = value.as_str()?;
|
|
DateTime::parse_from_rfc3339(raw)
|
|
.ok()
|
|
.map(|dt: DateTime<FixedOffset>| dt.timestamp_millis())
|
|
}
|
|
|
|
pub fn extract_text(content: &Value) -> String {
|
|
match content {
|
|
Value::String(text) => text.to_string(),
|
|
Value::Array(items) => items
|
|
.iter()
|
|
.filter_map(extract_text_from_item)
|
|
.filter(|text| !text.trim().is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
Value::Object(map) => map
|
|
.get("text")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or_default()
|
|
.to_string(),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
fn extract_text_from_item(item: &Value) -> Option<String> {
|
|
let item_type = item.get("type").and_then(Value::as_str).unwrap_or("");
|
|
|
|
// tool_use: show tool name
|
|
if item_type == "tool_use" {
|
|
let name = item
|
|
.get("name")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("unknown");
|
|
return Some(format!("[Tool: {name}]"));
|
|
}
|
|
|
|
// tool_result: extract nested content
|
|
if item_type == "tool_result" {
|
|
if let Some(content) = item.get("content") {
|
|
let text = extract_text(content);
|
|
if !text.is_empty() {
|
|
return Some(text);
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if let Some(text) = item.get("text").and_then(|v| v.as_str()) {
|
|
return Some(text.to_string());
|
|
}
|
|
|
|
if let Some(text) = item.get("input_text").and_then(|v| v.as_str()) {
|
|
return Some(text.to_string());
|
|
}
|
|
|
|
if let Some(text) = item.get("output_text").and_then(|v| v.as_str()) {
|
|
return Some(text.to_string());
|
|
}
|
|
|
|
if let Some(content) = item.get("content") {
|
|
let text = extract_text(content);
|
|
if !text.is_empty() {
|
|
return Some(text);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn truncate_summary(text: &str, max_chars: usize) -> String {
|
|
let trimmed = text.trim();
|
|
if trimmed.is_empty() {
|
|
return String::new();
|
|
}
|
|
if trimmed.chars().count() <= max_chars {
|
|
return trimmed.to_string();
|
|
}
|
|
|
|
let mut result = trimmed.chars().take(max_chars).collect::<String>();
|
|
result.push_str("...");
|
|
result
|
|
}
|
|
|
|
pub fn path_basename(value: &str) -> Option<String> {
|
|
let trimmed = value.trim();
|
|
if trimmed.is_empty() {
|
|
return None;
|
|
}
|
|
let normalized = trimmed.trim_end_matches(['/', '\\']);
|
|
let last = normalized
|
|
.split(['/', '\\'])
|
|
.next_back()
|
|
.filter(|segment| !segment.is_empty())?;
|
|
Some(last.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn parse_timestamp_to_ms_supports_integers_and_rfc3339() {
|
|
assert_eq!(
|
|
parse_timestamp_to_ms(&json!(1_771_061_953_033_i64)),
|
|
Some(1_771_061_953_033)
|
|
);
|
|
assert_eq!(
|
|
parse_timestamp_to_ms(&json!(1_771_061_953_i64)),
|
|
Some(1_771_061_953_000)
|
|
);
|
|
assert_eq!(
|
|
parse_timestamp_to_ms(&json!("1970-01-01T00:00:01Z")),
|
|
Some(1_000)
|
|
);
|
|
}
|
|
}
|