1use crate::constants::{
2 DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_MAX_HISTORY_MESSAGES, DEFAULT_MAX_TOOL_ROUNDS,
3};
4use crate::context::compact::CompactConfig;
5use crate::theme_name::ThemeName;
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ModelProvider {
13 pub name: String,
15 pub api_base: String,
17 pub api_key: String,
19 pub model: String,
21 #[serde(default)]
23 pub supports_vision: bool,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "snake_case")]
29pub enum ThinkingStyle {
30 #[default]
32 Braille,
33 Classic,
35 Pulse,
37 Wave,
39 Blink,
41 Comet,
43}
44
45impl ThinkingStyle {
46 pub const ALL: &[ThinkingStyle] = &[
48 ThinkingStyle::Braille,
49 ThinkingStyle::Classic,
50 ThinkingStyle::Pulse,
51 ThinkingStyle::Wave,
52 ThinkingStyle::Blink,
53 ThinkingStyle::Comet,
54 ];
55
56 pub fn display_name(&self) -> &'static str {
58 match self {
59 Self::Braille => "旋转点阵",
60 Self::Classic => "经典圆点",
61 Self::Pulse => "呼吸圆点",
62 Self::Wave => "波浪三连",
63 Self::Blink => "闪烁光标",
64 Self::Comet => "渐变彗星",
65 }
66 }
67
68 pub fn as_str(&self) -> &'static str {
70 match self {
71 Self::Braille => "braille",
72 Self::Classic => "classic",
73 Self::Pulse => "pulse",
74 Self::Wave => "wave",
75 Self::Blink => "blink",
76 Self::Comet => "comet",
77 }
78 }
79
80 pub fn parse(s: &str) -> Self {
82 match s.trim().to_lowercase().as_str() {
83 "braille" => Self::Braille,
84 "classic" => Self::Classic,
85 "pulse" => Self::Pulse,
86 "wave" => Self::Wave,
87 "blink" => Self::Blink,
88 "comet" => Self::Comet,
89 "旋转点阵" => Self::Braille,
91 "经典圆点" => Self::Classic,
92 "呼吸圆点" => Self::Pulse,
93 "波浪三连" => Self::Wave,
94 "闪烁光标" => Self::Blink,
95 "渐变彗星" => Self::Comet,
96 _ => Self::default(),
97 }
98 }
99
100 pub fn next(&self) -> Self {
102 let idx = Self::ALL.iter().position(|s| s == self).unwrap_or(0);
103 Self::ALL[(idx + 1) % Self::ALL.len()]
104 }
105
106 pub fn frame(&self, tick: u64) -> &'static str {
108 match self {
109 Self::Braille => {
110 const FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
111 FRAMES[(tick as usize) % FRAMES.len()]
112 }
113 Self::Classic => "◍",
114 Self::Pulse => {
115 const FRAMES: &[&str] = &["·", "◦", "○", "◔", "◕", "●", "◕", "◔", "○", "◦"];
116 FRAMES[(tick as usize) % FRAMES.len()]
117 }
118 Self::Wave => {
119 const FRAMES: &[&str] = &["● · ·", "· ● ·", "· · ●", "· ● ·"];
120 FRAMES[(tick as usize) % FRAMES.len()]
121 }
122 Self::Blink => {
123 const FRAMES: &[&str] = &["█", " "];
124 FRAMES[(tick as usize / 5) % FRAMES.len()]
125 }
126 Self::Comet => {
127 const FRAMES: &[&str] = &[
129 "██▓▒░ ",
130 " ██▓▒░ ",
131 " ██▓▒░ ",
132 " ██▓▒░ ",
133 " ██▓▒░ ",
134 " ██▓▒░ ",
135 " ██▓▒░ ",
136 " ██▓▒░ ",
137 " ██▓▒░",
138 " ██▓▒░ ",
139 " ██▓▒░ ",
140 " ██▓▒░ ",
141 " ██▓▒░ ",
142 " ██▓▒░ ",
143 " ██▓▒░ ",
144 " ██▓▒░ ",
145 ];
146 FRAMES[(tick as usize) % FRAMES.len()]
147 }
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AgentConfig {
155 #[serde(default)]
157 pub providers: Vec<ModelProvider>,
158 #[serde(default)]
160 pub active_index: usize,
161 #[serde(default)]
163 pub system_prompt: Option<String>,
164 #[serde(default = "default_max_history_messages")]
166 pub max_history_messages: usize,
167 #[serde(default = "default_max_context_tokens")]
169 pub max_context_tokens: usize,
170 #[serde(default)]
172 pub theme: ThemeName,
173 #[serde(default)]
175 pub tools_enabled: bool,
176 #[serde(default = "default_max_tool_rounds")]
178 pub max_tool_rounds: usize,
179 #[serde(default)]
181 pub style: Option<String>,
182 #[serde(default)]
184 pub tool_confirm_timeout: u64,
185 #[serde(default)]
187 pub disabled_tools: Vec<String>,
188 #[serde(default = "default_deferred_tools")]
190 pub deferred_tools: Vec<String>,
191 #[serde(default)]
193 pub disabled_skills: Vec<String>,
194 #[serde(default)]
196 pub disabled_commands: Vec<String>,
197 #[serde(default)]
199 pub disabled_hooks: Vec<String>,
200 #[serde(default)]
202 pub compact: CompactConfig,
203 #[serde(default)]
205 pub auto_restore_session: bool,
206 #[serde(default = "default_true")]
208 pub flat_bubble: bool,
209 #[serde(default)]
211 pub thinking_style: ThinkingStyle,
212 #[serde(default = "default_true")]
214 pub welcome_quote: bool,
215}
216
217fn default_max_history_messages() -> usize {
218 DEFAULT_MAX_HISTORY_MESSAGES
219}
220
221fn default_max_context_tokens() -> usize {
222 DEFAULT_MAX_CONTEXT_TOKENS
223}
224
225fn default_max_tool_rounds() -> usize {
226 DEFAULT_MAX_TOOL_ROUNDS
227}
228
229fn default_true() -> bool {
230 true
231}
232
233fn default_deferred_tools() -> Vec<String> {
234 vec![
235 "Task".to_string(),
236 "RegisterHook".to_string(),
237 "ComputerUse".to_string(),
238 "Browser".to_string(),
239 ]
240}
241
242impl Default for AgentConfig {
243 fn default() -> Self {
244 Self {
245 providers: Vec::new(),
246 active_index: 0,
247 system_prompt: None,
248 max_history_messages: DEFAULT_MAX_HISTORY_MESSAGES,
249 max_context_tokens: DEFAULT_MAX_CONTEXT_TOKENS,
250 theme: ThemeName::default(),
251 tools_enabled: false,
252 max_tool_rounds: DEFAULT_MAX_TOOL_ROUNDS,
253 style: None,
254 tool_confirm_timeout: 0,
255 disabled_tools: Vec::new(),
256 deferred_tools: default_deferred_tools(),
257 disabled_skills: Vec::new(),
258 disabled_commands: Vec::new(),
259 disabled_hooks: Vec::new(),
260 compact: CompactConfig::default(),
261 auto_restore_session: false,
262 flat_bubble: true,
263 thinking_style: ThinkingStyle::default(),
264 welcome_quote: true,
265 }
266 }
267}
268
269fn load_text_file(path: &Path) -> Option<String> {
273 if !path.exists() {
274 return None;
275 }
276 match fs::read_to_string(path) {
277 Ok(content) => {
278 let trimmed = content.trim();
279 if trimmed.is_empty() {
280 None
281 } else {
282 Some(trimmed.to_string())
283 }
284 }
285 Err(e) => {
286 eprintln!("[ERROR] ✖️ 读取 {} 失败: {}", path.display(), e);
287 None
288 }
289 }
290}
291
292fn save_text_file(path: &Path, content: &str) -> bool {
294 if let Some(parent) = path.parent() {
295 let _ = fs::create_dir_all(parent);
296 }
297
298 let trimmed = content.trim();
299 if trimmed.is_empty() {
300 return match fs::remove_file(path) {
301 Ok(_) => true,
302 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
303 Err(e) => {
304 eprintln!("[ERROR] ✖️ 删除 {} 失败: {}", path.display(), e);
305 false
306 }
307 };
308 }
309
310 match fs::write(path, trimmed) {
311 Ok(_) => true,
312 Err(e) => {
313 eprintln!("[ERROR] ✖️ 保存 {} 失败: {}", path.display(), e);
314 false
315 }
316 }
317}
318
319pub fn agent_data_dir() -> PathBuf {
321 let dir = crate::constants::data_root().join("agent").join("data");
322 let _ = fs::create_dir_all(&dir);
323 dir
324}
325
326pub fn agent_config_path() -> PathBuf {
328 agent_data_dir().join("agent_config.json")
329}
330
331pub fn system_prompt_path() -> PathBuf {
333 agent_data_dir().join("system_prompt.md")
334}
335
336pub fn style_path() -> PathBuf {
338 agent_data_dir().join("style.md")
339}
340
341pub fn memory_path() -> PathBuf {
343 agent_data_dir().join("memory.md")
344}
345
346pub fn soul_path() -> PathBuf {
348 agent_data_dir().join("soul.md")
349}
350
351pub fn load_agent_config() -> AgentConfig {
353 let path = agent_config_path();
354 if !path.exists() {
355 return AgentConfig::default();
356 }
357 match fs::read_to_string(&path) {
358 Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
359 eprintln!("[ERROR] ✖️ 解析 agent_config.json 失败: {}", e);
360 AgentConfig::default()
361 }),
362 Err(e) => {
363 eprintln!("[ERROR] ✖️ 读取 agent_config.json 失败: {}", e);
364 AgentConfig::default()
365 }
366 }
367}
368
369pub fn save_agent_config(config: &AgentConfig) -> bool {
371 let path = agent_config_path();
372 if let Some(parent) = path.parent() {
373 let _ = fs::create_dir_all(parent);
374 }
375 let mut config_to_save = config.clone();
377 config_to_save.system_prompt = None;
378 config_to_save.style = None;
379 match serde_json::to_string_pretty(&config_to_save) {
380 Ok(json) => match fs::write(&path, json) {
381 Ok(_) => true,
382 Err(e) => {
383 eprintln!("[ERROR] ✖️ 保存 agent_config.json 失败: {}", e);
384 false
385 }
386 },
387 Err(e) => {
388 eprintln!("[ERROR] ✖️ 序列化 agent 配置失败: {}", e);
389 false
390 }
391 }
392}
393
394pub fn load_system_prompt() -> Option<String> {
396 load_text_file(&system_prompt_path())
397}
398
399pub fn save_system_prompt(prompt: &str) -> bool {
401 save_text_file(&system_prompt_path(), prompt)
402}
403
404pub fn load_style() -> Option<String> {
406 load_text_file(&style_path())
407}
408
409pub fn save_style(style: &str) -> bool {
411 save_text_file(&style_path(), style)
412}
413
414pub fn load_memory() -> Option<String> {
416 load_text_file(&memory_path())
417}
418
419pub fn load_soul() -> Option<String> {
421 load_text_file(&soul_path())
422}
423
424pub fn save_memory(content: &str) -> bool {
426 let path = memory_path();
427 if let Some(parent) = path.parent() {
428 let _ = fs::create_dir_all(parent);
429 }
430 match fs::write(path, content) {
431 Ok(_) => true,
432 Err(e) => {
433 eprintln!("[ERROR] ✖️ 保存 memory.md 失败: {}", e);
434 false
435 }
436 }
437}
438
439pub fn save_soul(content: &str) -> bool {
441 let path = soul_path();
442 if let Some(parent) = path.parent() {
443 let _ = fs::create_dir_all(parent);
444 }
445 match fs::write(path, content) {
446 Ok(_) => true,
447 Err(e) => {
448 eprintln!("[ERROR] ✖️ 保存 soul.md 失败: {}", e);
449 false
450 }
451 }
452}