mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 10:25:05 +08:00
165af5eec4
Add two new session providers following the existing convention-based pattern. OpenCode reads three-layer JSON storage, OpenClaw parses JSONL event streams. Wire up backend dispatch, frontend filter dropdown, icon mappings, toolbar button for OpenClaw, and i18n subtitle updates.
212 lines
5.8 KiB
Rust
212 lines
5.8 KiB
Rust
use std::fs::File;
|
|
use std::io::{BufRead, BufReader};
|
|
use std::path::Path;
|
|
|
|
use serde_json::Value;
|
|
|
|
use crate::openclaw_config::get_openclaw_dir;
|
|
use crate::session_manager::{SessionMessage, SessionMeta};
|
|
|
|
use super::utils::{extract_text, parse_timestamp_to_ms, path_basename, truncate_summary};
|
|
|
|
const PROVIDER_ID: &str = "openclaw";
|
|
|
|
pub fn scan_sessions() -> Vec<SessionMeta> {
|
|
let agents_dir = get_openclaw_dir().join("agents");
|
|
if !agents_dir.exists() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let mut sessions = Vec::new();
|
|
|
|
// Traverse each agent directory
|
|
let agent_entries = match std::fs::read_dir(&agents_dir) {
|
|
Ok(entries) => entries,
|
|
Err(_) => return sessions,
|
|
};
|
|
|
|
for agent_entry in agent_entries.flatten() {
|
|
let agent_path = agent_entry.path();
|
|
if !agent_path.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let sessions_dir = agent_path.join("sessions");
|
|
if !sessions_dir.is_dir() {
|
|
continue;
|
|
}
|
|
|
|
let session_entries = match std::fs::read_dir(&sessions_dir) {
|
|
Ok(entries) => entries,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
for entry in session_entries.flatten() {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|ext| ext.to_str()) != Some("jsonl") {
|
|
continue;
|
|
}
|
|
// Skip sessions.json index file
|
|
if path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.map(|n| n == "sessions.json")
|
|
.unwrap_or(false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if let Some(meta) = parse_session(&path) {
|
|
sessions.push(meta);
|
|
}
|
|
}
|
|
}
|
|
|
|
sessions
|
|
}
|
|
|
|
pub fn load_messages(path: &Path) -> Result<Vec<SessionMessage>, String> {
|
|
let file = File::open(path).map_err(|e| format!("Failed to open session file: {e}"))?;
|
|
let reader = BufReader::new(file);
|
|
let mut messages = Vec::new();
|
|
|
|
for line in reader.lines() {
|
|
let line = match line {
|
|
Ok(value) => value,
|
|
Err(_) => continue,
|
|
};
|
|
let value: Value = match serde_json::from_str(&line) {
|
|
Ok(parsed) => parsed,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if value.get("type").and_then(Value::as_str) != Some("message") {
|
|
continue;
|
|
}
|
|
|
|
let message = match value.get("message") {
|
|
Some(msg) => msg,
|
|
None => continue,
|
|
};
|
|
|
|
let raw_role = message
|
|
.get("role")
|
|
.and_then(Value::as_str)
|
|
.unwrap_or("unknown");
|
|
|
|
// Map OpenClaw roles to our standard roles
|
|
let role = match raw_role {
|
|
"toolResult" => "tool".to_string(),
|
|
other => other.to_string(),
|
|
};
|
|
|
|
let content = message.get("content").map(extract_text).unwrap_or_default();
|
|
if content.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let ts = value.get("timestamp").and_then(parse_timestamp_to_ms);
|
|
|
|
messages.push(SessionMessage { role, content, ts });
|
|
}
|
|
|
|
Ok(messages)
|
|
}
|
|
|
|
fn parse_session(path: &Path) -> Option<SessionMeta> {
|
|
let file = File::open(path).ok()?;
|
|
let reader = BufReader::new(file);
|
|
|
|
let mut session_id: Option<String> = None;
|
|
let mut cwd: Option<String> = None;
|
|
let mut created_at: Option<i64> = None;
|
|
let mut last_active_at: Option<i64> = None;
|
|
let mut summary: Option<String> = None;
|
|
|
|
for line in reader.lines() {
|
|
let line = match line {
|
|
Ok(value) => value,
|
|
Err(_) => continue,
|
|
};
|
|
let value: Value = match serde_json::from_str(&line) {
|
|
Ok(parsed) => parsed,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
|
|
if created_at.is_none() {
|
|
created_at = Some(ts);
|
|
}
|
|
last_active_at = Some(ts);
|
|
}
|
|
|
|
let event_type = value.get("type").and_then(Value::as_str).unwrap_or("");
|
|
|
|
if event_type == "session" {
|
|
if session_id.is_none() {
|
|
session_id = value
|
|
.get("id")
|
|
.and_then(Value::as_str)
|
|
.map(|s| s.to_string());
|
|
}
|
|
if cwd.is_none() {
|
|
cwd = value
|
|
.get("cwd")
|
|
.and_then(Value::as_str)
|
|
.map(|s| s.to_string());
|
|
}
|
|
if let Some(ts) = value.get("timestamp").and_then(parse_timestamp_to_ms) {
|
|
created_at.get_or_insert(ts);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if event_type != "message" {
|
|
continue;
|
|
}
|
|
|
|
// Extract first message content for summary
|
|
if summary.is_some() {
|
|
continue;
|
|
}
|
|
|
|
let message = match value.get("message") {
|
|
Some(msg) => msg,
|
|
None => continue,
|
|
};
|
|
|
|
let text = message.get("content").map(extract_text).unwrap_or_default();
|
|
if text.trim().is_empty() {
|
|
continue;
|
|
}
|
|
summary = Some(text);
|
|
}
|
|
|
|
// Fall back to filename as session ID
|
|
let session_id = session_id.or_else(|| {
|
|
path.file_stem()
|
|
.and_then(|s| s.to_str())
|
|
.map(|s| s.to_string())
|
|
});
|
|
let session_id = session_id?;
|
|
|
|
let title = cwd
|
|
.as_deref()
|
|
.and_then(path_basename)
|
|
.map(|s| s.to_string());
|
|
|
|
let summary = summary.map(|text| truncate_summary(&text, 160));
|
|
|
|
Some(SessionMeta {
|
|
provider_id: PROVIDER_ID.to_string(),
|
|
session_id: session_id.clone(),
|
|
title,
|
|
summary,
|
|
project_dir: cwd,
|
|
created_at,
|
|
last_active_at,
|
|
source_path: Some(path.to_string_lossy().to_string()),
|
|
resume_command: None, // OpenClaw sessions are gateway-managed, no CLI resume
|
|
})
|
|
}
|