Skip to main content

j_agent/storage/
config.rs

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/// 单个模型提供方配置
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ModelProvider {
13    /// 显示名称(如 "GPT-4o", "DeepSeek-V3")
14    pub name: String,
15    /// API Base URL(如 "https://api.openai.com/v1")
16    pub api_base: String,
17    /// API Key
18    pub api_key: String,
19    /// 模型名称(如 "gpt-4o", "deepseek-chat")
20    pub model: String,
21    /// 是否支持视觉/多模态(默认 false)
22    #[serde(default)]
23    pub supports_vision: bool,
24}
25
26/// 思考指示器动画风格
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "snake_case")]
29pub enum ThinkingStyle {
30    /// Braille 点阵旋转(默认)
31    #[default]
32    Braille,
33    /// 经典圆点(原版 ◍ + 颜色脉冲)
34    Classic,
35    /// 圆环呼吸(渐变大小)
36    Pulse,
37    /// 三点波浪
38    Wave,
39    /// 光标闪烁
40    Blink,
41    /// 渐变彗星(拖尾字符密度渐变)
42    Comet,
43}
44
45impl ThinkingStyle {
46    /// 所有可能值,用于 config panel 循环切换
47    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    /// 显示名称(中文)
57    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    /// 序列化名称
69    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    /// 从字符串解析,支持英文标识和中文名
81    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            // 中文名映射
90            "旋转点阵" => Self::Braille,
91            "经典圆点" => Self::Classic,
92            "呼吸圆点" => Self::Pulse,
93            "波浪三连" => Self::Wave,
94            "闪烁光标" => Self::Blink,
95            "渐变彗星" => Self::Comet,
96            _ => Self::default(),
97        }
98    }
99
100    /// 切换到下一个风格
101    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    /// 基于 tick(每 100ms 递增 1)返回当前帧的显示字符
107    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                // 宽度 13 的轨道上,密度渐变的 "██▓▒░" 彗星左右来回弹跳(ping-pong)
128                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/// Agent 配置
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AgentConfig {
155    /// 模型提供方列表
156    #[serde(default)]
157    pub providers: Vec<ModelProvider>,
158    /// 当前选中的 provider 索引
159    #[serde(default)]
160    pub active_index: usize,
161    /// 系统提示词(可选)
162    #[serde(default)]
163    pub system_prompt: Option<String>,
164    /// 发送给 API 的历史消息数量限制(默认 20 条,避免 token 消耗过大)
165    #[serde(default = "default_max_history_messages")]
166    pub max_history_messages: usize,
167    /// 上下文 token 预算(优先级选择时的 token 上限,默认 100K)
168    #[serde(default = "default_max_context_tokens")]
169    pub max_context_tokens: usize,
170    /// 主题名称(dark / light / midnight)
171    #[serde(default)]
172    pub theme: ThemeName,
173    /// 是否启用工具调用(默认关闭)
174    #[serde(default)]
175    pub tools_enabled: bool,
176    /// 工具调用最大轮数(默认 10,防止无限循环)
177    #[serde(default = "default_max_tool_rounds")]
178    pub max_tool_rounds: usize,
179    /// 回复风格(可选)
180    #[serde(default)]
181    pub style: Option<String>,
182    /// 工具确认超时秒数(0 表示不超时,需手动确认;>0 则超时后自动执行)
183    #[serde(default)]
184    pub tool_confirm_timeout: u64,
185    /// 被禁用的工具名称列表(tools_enabled=true 时,此列表中的工具不会发送给 LLM)
186    #[serde(default)]
187    pub disabled_tools: Vec<String>,
188    /// 延迟加载的工具名称列表(需要 LoadTool 加载后才可用)
189    #[serde(default = "default_deferred_tools")]
190    pub deferred_tools: Vec<String>,
191    /// 被禁用的 skill 名称列表(列表中的 skill 不会包含在系统提示词中)
192    #[serde(default)]
193    pub disabled_skills: Vec<String>,
194    /// 被禁用的 command 名称列表
195    #[serde(default)]
196    pub disabled_commands: Vec<String>,
197    /// 被禁用的 hook 标识列表(格式:`source:unique_id`,如 `user:my_hook`、`session:0`)
198    #[serde(default)]
199    pub disabled_hooks: Vec<String>,
200    /// Context compact 配置
201    #[serde(default)]
202    pub compact: CompactConfig,
203    /// 启动时是否自动恢复最近的 session
204    #[serde(default)]
205    pub auto_restore_session: bool,
206    /// 气泡背景色与主背景色一致(扁平效果)
207    #[serde(default = "default_true")]
208    pub flat_bubble: bool,
209    /// 思考指示器动画风格
210    #[serde(default)]
211    pub thinking_style: ThinkingStyle,
212    /// 欢迎界面是否显示诗句
213    #[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
269// ========== 通用文本文件读写辅助 ==========
270
271/// 从文件加载文本内容,trim 后返回;文件不存在或内容为空返回 None
272fn 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
292/// 保存文本内容到文件(空字符串删除文件)
293fn 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
319/// 获取 agent 数据目录: ~/.jdata/agent/data/
320pub 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
326/// 获取 agent 配置文件路径
327pub fn agent_config_path() -> PathBuf {
328    agent_data_dir().join("agent_config.json")
329}
330
331/// 获取系统提示词文件路径
332pub fn system_prompt_path() -> PathBuf {
333    agent_data_dir().join("system_prompt.md")
334}
335
336/// 获取回复风格文件路径
337pub fn style_path() -> PathBuf {
338    agent_data_dir().join("style.md")
339}
340
341/// 获取记忆文件路径
342pub fn memory_path() -> PathBuf {
343    agent_data_dir().join("memory.md")
344}
345
346/// 获取灵魂文件路径
347pub fn soul_path() -> PathBuf {
348    agent_data_dir().join("soul.md")
349}
350
351/// 加载 Agent 配置
352pub 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
369/// 保存 Agent 配置
370pub 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    // system_prompt 和 style 统一存放在独立文件,不再写入 agent_config.json
376    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
394/// 加载系统提示词(来自独立文件)
395pub fn load_system_prompt() -> Option<String> {
396    load_text_file(&system_prompt_path())
397}
398
399/// 保存系统提示词到独立文件(空字符串会删除文件)
400pub fn save_system_prompt(prompt: &str) -> bool {
401    save_text_file(&system_prompt_path(), prompt)
402}
403
404/// 加载回复风格(来自独立文件)
405pub fn load_style() -> Option<String> {
406    load_text_file(&style_path())
407}
408
409/// 保存回复风格到独立文件(空字符串会删除文件)
410pub fn save_style(style: &str) -> bool {
411    save_text_file(&style_path(), style)
412}
413
414/// 加载记忆(来自独立文件)
415pub fn load_memory() -> Option<String> {
416    load_text_file(&memory_path())
417}
418
419/// 加载灵魂(来自独立文件)
420pub fn load_soul() -> Option<String> {
421    load_text_file(&soul_path())
422}
423
424/// 保存记忆到独立文件
425pub 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
439/// 保存灵魂到独立文件
440pub 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}