use super::compact::CompactConfig;
use super::constants::{
DEFAULT_MAX_HISTORY_MESSAGES, DEFAULT_MAX_TOOL_ROUNDS, MESSAGE_PREVIEW_MAX_LEN,
};
use super::theme::ThemeName;
use crate::config::YamlConfig;
use crate::error;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelProvider {
pub name: String,
pub api_base: String,
pub api_key: String,
pub model: String,
#[serde(default)]
pub supports_vision: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AgentConfig {
#[serde(default)]
pub providers: Vec<ModelProvider>,
#[serde(default)]
pub active_index: usize,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default = "default_max_history_messages")]
pub max_history_messages: usize,
#[serde(default)]
pub theme: ThemeName,
#[serde(default)]
pub tools_enabled: bool,
#[serde(default = "default_max_tool_rounds")]
pub max_tool_rounds: usize,
#[serde(default)]
pub style: Option<String>,
#[serde(default)]
pub tool_confirm_timeout: u64,
#[serde(default)]
pub disabled_tools: Vec<String>,
#[serde(default)]
pub disabled_skills: Vec<String>,
#[serde(default)]
pub disabled_commands: Vec<String>,
#[serde(default)]
pub compact: CompactConfig,
#[serde(default)]
pub auto_restore_session: bool,
}
fn default_max_history_messages() -> usize {
DEFAULT_MAX_HISTORY_MESSAGES
}
fn default_max_tool_rounds() -> usize {
DEFAULT_MAX_TOOL_ROUNDS
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallItem {
pub id: String,
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImageData {
pub base64: String,
pub media_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String, #[serde(default)]
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option<Vec<ToolCallItem>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip)]
pub images: Option<Vec<ImageData>>,
}
impl ChatMessage {
pub fn text(role: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: role.into(),
content: content.into(),
tool_calls: None,
tool_call_id: None,
images: None,
}
}
#[allow(dead_code)]
pub fn with_images(
role: impl Into<String>,
content: impl Into<String>,
images: Vec<ImageData>,
) -> Self {
Self {
role: role.into(),
content: content.into(),
tool_calls: None,
tool_call_id: None,
images: if images.is_empty() {
None
} else {
Some(images)
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ChatSession {
pub messages: Vec<ChatMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SessionEvent {
Msg(ChatMessage),
Clear,
Restore { messages: Vec<ChatMessage> },
}
pub fn agent_data_dir() -> PathBuf {
let dir = YamlConfig::data_dir().join("agent").join("data");
let _ = fs::create_dir_all(&dir);
dir
}
pub fn sessions_dir() -> PathBuf {
let dir = agent_data_dir().join("sessions");
let _ = fs::create_dir_all(&dir);
dir
}
pub fn session_file_path(session_id: &str) -> PathBuf {
sessions_dir().join(format!("{}.jsonl", session_id))
}
pub fn agent_config_path() -> PathBuf {
agent_data_dir().join("agent_config.json")
}
pub fn legacy_chat_history_path() -> PathBuf {
agent_data_dir().join("chat_history.json")
}
pub fn system_prompt_path() -> PathBuf {
agent_data_dir().join("system_prompt.md")
}
pub fn style_path() -> PathBuf {
agent_data_dir().join("style.md")
}
pub fn memory_path() -> PathBuf {
agent_data_dir().join("memory.md")
}
pub fn soul_path() -> PathBuf {
agent_data_dir().join("soul.md")
}
pub fn hooks_config_path() -> PathBuf {
let dir = YamlConfig::data_dir().join("agent");
let _ = fs::create_dir_all(&dir);
dir.join("hooks.yaml")
}
pub fn load_agent_config() -> AgentConfig {
let path = agent_config_path();
if !path.exists() {
return AgentConfig::default();
}
match fs::read_to_string(&path) {
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
error!("✖️ 解析 agent_config.json 失败: {}", e);
AgentConfig::default()
}),
Err(e) => {
error!("✖️ 读取 agent_config.json 失败: {}", e);
AgentConfig::default()
}
}
}
pub fn save_agent_config(config: &AgentConfig) -> bool {
let path = agent_config_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let mut config_to_save = config.clone();
config_to_save.system_prompt = None;
config_to_save.style = None;
match serde_json::to_string_pretty(&config_to_save) {
Ok(json) => match fs::write(&path, json) {
Ok(_) => true,
Err(e) => {
error!("✖️ 保存 agent_config.json 失败: {}", e);
false
}
},
Err(e) => {
error!("✖️ 序列化 agent 配置失败: {}", e);
false
}
}
}
pub fn append_session_event(session_id: &str, event: &SessionEvent) -> bool {
let path = session_file_path(session_id);
match serde_json::to_string(event) {
Ok(line) => match fs::OpenOptions::new().create(true).append(true).open(&path) {
Ok(mut file) => writeln!(file, "{}", line).is_ok(),
Err(_) => false,
},
Err(_) => false,
}
}
pub fn find_latest_session_id() -> Option<String> {
let dir = sessions_dir();
let mut entries: Vec<(std::time::SystemTime, String)> = Vec::new();
let read_dir = match fs::read_dir(&dir) {
Ok(rd) => rd,
Err(_) => return None,
};
for entry in read_dir.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
if let (Ok(meta), Some(stem)) = (path.metadata(), path.file_stem().and_then(|s| s.to_str()))
&& let Ok(modified) = meta.modified()
{
entries.push((modified, stem.to_string()));
}
}
entries.sort_by(|a, b| b.0.cmp(&a.0));
entries.into_iter().next().map(|(_, id)| id)
}
fn repair_tool_call_ids(messages: &mut [ChatMessage]) {
use rand::Rng;
let mut i = 0;
while i < messages.len() {
let has_tool_calls = messages[i].role == "assistant"
&& messages[i]
.tool_calls
.as_ref()
.is_some_and(|tc| !tc.is_empty());
if !has_tool_calls {
i += 1;
continue;
}
let call_count = messages[i].tool_calls.as_ref().map_or(0, |tc| tc.len());
let result_start = i + 1;
let mut result_end = result_start;
while result_end < messages.len() && messages[result_end].role == "tool" {
result_end += 1;
}
let result_count = result_end - result_start;
if result_count == call_count {
for k in 0..call_count {
let result_idx = result_start + k;
let call_id = messages[i].tool_calls.as_ref().unwrap()[k].id.clone();
let result_id = messages[result_idx]
.tool_call_id
.clone()
.unwrap_or_default();
match (call_id.is_empty(), result_id.is_empty()) {
(true, true) => {
let new_id = format!("call_{:016x}", rand::thread_rng().r#gen::<u64>());
messages[i].tool_calls.as_mut().unwrap()[k].id = new_id.clone();
messages[result_idx].tool_call_id = Some(new_id);
}
(true, false) => {
messages[i].tool_calls.as_mut().unwrap()[k].id = result_id;
}
(false, true) => {
messages[result_idx].tool_call_id = Some(call_id);
}
(false, false) if call_id != result_id => {
messages[result_idx].tool_call_id = Some(call_id);
}
_ => {} }
}
}
i = result_end; }
}
pub fn load_session(session_id: &str) -> ChatSession {
let path = session_file_path(session_id);
if !path.exists() {
return ChatSession::default();
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => return ChatSession::default(),
};
let mut messages: Vec<ChatMessage> = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<SessionEvent>(line) {
Ok(event) => match event {
SessionEvent::Msg(msg) => messages.push(msg),
SessionEvent::Clear => messages.clear(),
SessionEvent::Restore { messages: restored } => messages = restored,
},
Err(_) => {
}
}
}
repair_tool_call_ids(&mut messages);
ChatSession { messages }
}
pub fn load_system_prompt() -> Option<String> {
let path = system_prompt_path();
if !path.exists() {
return None;
}
match fs::read_to_string(path) {
Ok(content) => {
let trimmed = content.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
Err(e) => {
error!("✖️ 读取 system_prompt.md 失败: {}", e);
None
}
}
}
pub fn save_system_prompt(prompt: &str) -> bool {
let path = system_prompt_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let trimmed = prompt.trim();
if trimmed.is_empty() {
return match fs::remove_file(&path) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(e) => {
error!("✖️ 删除 system_prompt.md 失败: {}", e);
false
}
};
}
match fs::write(path, trimmed) {
Ok(_) => true,
Err(e) => {
error!("✖️ 保存 system_prompt.md 失败: {}", e);
false
}
}
}
pub fn load_style() -> Option<String> {
let path = style_path();
if !path.exists() {
return None;
}
match fs::read_to_string(path) {
Ok(content) => {
let trimmed = content.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
Err(e) => {
error!("✖️ 读取 style.md 失败: {}", e);
None
}
}
}
pub fn save_style(style: &str) -> bool {
let path = style_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
let trimmed = style.trim();
if trimmed.is_empty() {
return match fs::remove_file(&path) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(e) => {
error!("✖️ 删除 style.md 失败: {}", e);
false
}
};
}
match fs::write(path, trimmed) {
Ok(_) => true,
Err(e) => {
error!("✖️ 保存 style.md 失败: {}", e);
false
}
}
}
pub fn load_memory() -> Option<String> {
let path = memory_path();
if !path.exists() {
return None;
}
match fs::read_to_string(path) {
Ok(content) => {
let trimmed = content.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
Err(e) => {
error!("✖️ 读取 memory.md 失败: {}", e);
None
}
}
}
pub fn load_soul() -> Option<String> {
let path = soul_path();
if !path.exists() {
return None;
}
match fs::read_to_string(path) {
Ok(content) => {
let trimmed = content.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
Err(e) => {
error!("✖️ 读取 soul.md 失败: {}", e);
None
}
}
}
pub fn save_memory(content: &str) -> bool {
let path = memory_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
match fs::write(path, content) {
Ok(_) => true,
Err(e) => {
error!("✖️ 保存 memory.md 失败: {}", e);
false
}
}
}
pub fn save_soul(content: &str) -> bool {
let path = soul_path();
if let Some(parent) = path.parent() {
let _ = fs::create_dir_all(parent);
}
match fs::write(path, content) {
Ok(_) => true,
Err(e) => {
error!("✖️ 保存 soul.md 失败: {}", e);
false
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMeta {
pub id: String,
pub message_count: usize,
pub first_message_preview: Option<String>,
pub updated_at: u64,
}
pub fn list_sessions() -> Vec<SessionMeta> {
let dir = sessions_dir();
let read_dir = match fs::read_dir(&dir) {
Ok(rd) => rd,
Err(_) => return Vec::new(),
};
let mut sessions: Vec<SessionMeta> = Vec::new();
for entry in read_dir.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
continue;
}
let id = match path.file_stem().and_then(|s| s.to_str()) {
Some(s) => s.to_string(),
None => continue,
};
let updated_at = path
.metadata()
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0);
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue, };
let mut message_count: usize = 0;
let mut first_user_preview: Option<String> = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Ok(event) = serde_json::from_str::<SessionEvent>(line) {
match event {
SessionEvent::Msg(ref msg) => {
message_count += 1;
if first_user_preview.is_none()
&& msg.role == "user"
&& !msg.content.is_empty()
{
let preview: String =
msg.content.chars().take(MESSAGE_PREVIEW_MAX_LEN).collect();
first_user_preview = Some(preview);
}
}
SessionEvent::Clear => {
message_count = 0;
first_user_preview = None;
}
SessionEvent::Restore { ref messages } => {
message_count = messages.len();
first_user_preview = messages
.iter()
.find(|m| m.role == "user" && !m.content.is_empty())
.map(|m| m.content.chars().take(MESSAGE_PREVIEW_MAX_LEN).collect());
}
}
}
}
sessions.push(SessionMeta {
id,
message_count,
first_message_preview: first_user_preview,
updated_at,
});
}
sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
sessions
}
pub fn generate_session_id() -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_micros();
let pid = std::process::id();
format!("{:x}-{:x}", ts, pid)
}
pub fn delete_session(session_id: &str) -> bool {
let path = session_file_path(session_id);
match fs::remove_file(&path) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(_) => false,
}
}