fix: parse tool_use/tool_result messages and add OpenCode SQLite backend (#1401)

* 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>
This commit is contained in:
BlueOcean
2026-03-22 22:02:35 +08:00
committed by GitHub
parent 117dbf1386
commit bd3cfb7741
7 changed files with 858 additions and 35 deletions
@@ -46,6 +46,15 @@ pub fn read_head_tail_lines(
}
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()
@@ -71,6 +80,28 @@ pub fn extract_text(content: &Value) -> String {
}
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());
}
@@ -119,3 +150,25 @@ pub fn path_basename(value: &str) -> Option<String> {
.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)
);
}
}