use crate::command::chat::constants::{
DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_MAX_HISTORY_MESSAGES, DEFAULT_MAX_TOOL_ROUNDS,
};
use crate::command::chat::context::compact::CompactConfig;
use crate::config::YamlConfig;
use crate::error;
use crate::theme::ThemeName;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
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, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ThinkingStyle {
#[default]
Braille,
Classic,
Pulse,
Wave,
Blink,
Comet,
}
impl ThinkingStyle {
pub const ALL: &[ThinkingStyle] = &[
ThinkingStyle::Braille,
ThinkingStyle::Classic,
ThinkingStyle::Pulse,
ThinkingStyle::Wave,
ThinkingStyle::Blink,
ThinkingStyle::Comet,
];
pub fn display_name(&self) -> &'static str {
match self {
Self::Braille => "旋转点阵",
Self::Classic => "经典圆点",
Self::Pulse => "呼吸圆点",
Self::Wave => "波浪三连",
Self::Blink => "闪烁光标",
Self::Comet => "渐变彗星",
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Braille => "braille",
Self::Classic => "classic",
Self::Pulse => "pulse",
Self::Wave => "wave",
Self::Blink => "blink",
Self::Comet => "comet",
}
}
pub fn parse(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"braille" => Self::Braille,
"classic" => Self::Classic,
"pulse" => Self::Pulse,
"wave" => Self::Wave,
"blink" => Self::Blink,
"comet" => Self::Comet,
"旋转点阵" => Self::Braille,
"经典圆点" => Self::Classic,
"呼吸圆点" => Self::Pulse,
"波浪三连" => Self::Wave,
"闪烁光标" => Self::Blink,
"渐变彗星" => Self::Comet,
_ => Self::default(),
}
}
pub fn next(&self) -> Self {
let idx = Self::ALL.iter().position(|s| s == self).unwrap_or(0);
Self::ALL[(idx + 1) % Self::ALL.len()]
}
pub fn frame(&self, tick: u64) -> &'static str {
match self {
Self::Braille => {
const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
FRAMES[(tick as usize) % FRAMES.len()]
}
Self::Classic => "◍",
Self::Pulse => {
const FRAMES: &[&str] = &["·", "◦", "○", "◔", "◕", "●", "◕", "◔", "○", "◦"];
FRAMES[(tick as usize) % FRAMES.len()]
}
Self::Wave => {
const FRAMES: &[&str] = &["● · ·", "· ● ·", "· · ●", "· ● ·"];
FRAMES[(tick as usize) % FRAMES.len()]
}
Self::Blink => {
const FRAMES: &[&str] = &["█", " "];
FRAMES[(tick as usize / 5) % FRAMES.len()]
}
Self::Comet => {
const FRAMES: &[&str] = &[
"██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
" ██▓▒░ ",
];
FRAMES[(tick as usize) % FRAMES.len()]
}
}
}
}
#[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 = "default_max_context_tokens")]
pub max_context_tokens: 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 disabled_hooks: Vec<String>,
#[serde(default)]
pub compact: CompactConfig,
#[serde(default)]
pub auto_restore_session: bool,
#[serde(default)]
pub thinking_style: ThinkingStyle,
}
fn default_max_history_messages() -> usize {
DEFAULT_MAX_HISTORY_MESSAGES
}
fn default_max_context_tokens() -> usize {
DEFAULT_MAX_CONTEXT_TOKENS
}
fn default_max_tool_rounds() -> usize {
DEFAULT_MAX_TOOL_ROUNDS
}
pub fn agent_data_dir() -> PathBuf {
let dir = YamlConfig::data_dir().join("agent").join("data");
let _ = fs::create_dir_all(&dir);
dir
}
pub fn agent_config_path() -> PathBuf {
agent_data_dir().join("agent_config.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 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 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
}
}
}