j_cli/command/chat/
model.rs1use super::theme::ThemeName;
2use crate::config::YamlConfig;
3use crate::error;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ModelProvider {
13 pub name: String,
15 pub api_base: String,
17 pub api_key: String,
19 pub model: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct AgentConfig {
26 #[serde(default)]
28 pub providers: Vec<ModelProvider>,
29 #[serde(default)]
31 pub active_index: usize,
32 #[serde(default)]
34 pub system_prompt: Option<String>,
35 #[serde(default = "default_stream_mode")]
37 pub stream_mode: bool,
38 #[serde(default = "default_max_history_messages")]
40 pub max_history_messages: usize,
41 #[serde(default)]
43 pub theme: ThemeName,
44 #[serde(default)]
46 pub tools_enabled: bool,
47 #[serde(default = "default_max_tool_rounds")]
49 pub max_tool_rounds: usize,
50 #[serde(default)]
52 pub style: Option<String>,
53}
54
55fn default_max_history_messages() -> usize {
56 20
57}
58
59fn default_stream_mode() -> bool {
61 true
62}
63
64fn default_max_tool_rounds() -> usize {
66 10
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct ToolCallItem {
72 pub id: String,
73 pub name: String,
74 pub arguments: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ChatMessage {
80 pub role: String, #[serde(default)]
83 pub content: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub tool_calls: Option<Vec<ToolCallItem>>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub tool_call_id: Option<String>,
90}
91
92impl ChatMessage {
93 pub fn text(role: impl Into<String>, content: impl Into<String>) -> Self {
95 Self {
96 role: role.into(),
97 content: content.into(),
98 tool_calls: None,
99 tool_call_id: None,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct ChatSession {
107 pub messages: Vec<ChatMessage>,
108}
109
110pub fn agent_data_dir() -> PathBuf {
114 let dir = YamlConfig::data_dir().join("agent").join("data");
115 let _ = fs::create_dir_all(&dir);
116 dir
117}
118
119pub fn agent_config_path() -> PathBuf {
121 agent_data_dir().join("agent_config.json")
122}
123
124pub fn chat_history_path() -> PathBuf {
126 agent_data_dir().join("chat_history.json")
127}
128
129pub fn system_prompt_path() -> PathBuf {
131 agent_data_dir().join("system_prompt.md")
132}
133
134pub fn style_path() -> PathBuf {
136 agent_data_dir().join("style.md")
137}
138
139pub fn load_agent_config() -> AgentConfig {
143 let path = agent_config_path();
144 if !path.exists() {
145 return AgentConfig::default();
146 }
147 match fs::read_to_string(&path) {
148 Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
149 error!("❌ 解析 agent_config.json 失败: {}", e);
150 AgentConfig::default()
151 }),
152 Err(e) => {
153 error!("❌ 读取 agent_config.json 失败: {}", e);
154 AgentConfig::default()
155 }
156 }
157}
158
159pub fn save_agent_config(config: &AgentConfig) -> bool {
161 let path = agent_config_path();
162 if let Some(parent) = path.parent() {
163 let _ = fs::create_dir_all(parent);
164 }
165 let mut config_to_save = config.clone();
167 config_to_save.system_prompt = None;
168 config_to_save.style = None;
169 match serde_json::to_string_pretty(&config_to_save) {
170 Ok(json) => match fs::write(&path, json) {
171 Ok(_) => true,
172 Err(e) => {
173 error!("❌ 保存 agent_config.json 失败: {}", e);
174 false
175 }
176 },
177 Err(e) => {
178 error!("❌ 序列化 agent 配置失败: {}", e);
179 false
180 }
181 }
182}
183
184pub fn load_chat_session() -> ChatSession {
186 let path = chat_history_path();
187 if !path.exists() {
188 return ChatSession::default();
189 }
190 match fs::read_to_string(&path) {
191 Ok(content) => serde_json::from_str(&content).unwrap_or_else(|_| ChatSession::default()),
192 Err(_) => ChatSession::default(),
193 }
194}
195
196pub fn save_chat_session(session: &ChatSession) -> bool {
198 let path = chat_history_path();
199 if let Some(parent) = path.parent() {
200 let _ = fs::create_dir_all(parent);
201 }
202 match serde_json::to_string_pretty(session) {
203 Ok(json) => fs::write(&path, json).is_ok(),
204 Err(_) => false,
205 }
206}
207
208pub fn load_system_prompt() -> Option<String> {
210 let path = system_prompt_path();
211 if !path.exists() {
212 return None;
213 }
214 match fs::read_to_string(path) {
215 Ok(content) => {
216 let trimmed = content.trim();
217 if trimmed.is_empty() {
218 None
219 } else {
220 Some(trimmed.to_string())
221 }
222 }
223 Err(e) => {
224 error!("❌ 读取 system_prompt.md 失败: {}", e);
225 None
226 }
227 }
228}
229
230pub fn save_system_prompt(prompt: &str) -> bool {
232 let path = system_prompt_path();
233 if let Some(parent) = path.parent() {
234 let _ = fs::create_dir_all(parent);
235 }
236
237 let trimmed = prompt.trim();
238 if trimmed.is_empty() {
239 return match fs::remove_file(&path) {
240 Ok(_) => true,
241 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
242 Err(e) => {
243 error!("❌ 删除 system_prompt.md 失败: {}", e);
244 false
245 }
246 };
247 }
248
249 match fs::write(path, trimmed) {
250 Ok(_) => true,
251 Err(e) => {
252 error!("❌ 保存 system_prompt.md 失败: {}", e);
253 false
254 }
255 }
256}
257
258pub fn load_style() -> Option<String> {
260 let path = style_path();
261 if !path.exists() {
262 return None;
263 }
264 match fs::read_to_string(path) {
265 Ok(content) => {
266 let trimmed = content.trim();
267 if trimmed.is_empty() {
268 None
269 } else {
270 Some(trimmed.to_string())
271 }
272 }
273 Err(e) => {
274 error!("❌ 读取 style.md 失败: {}", e);
275 None
276 }
277 }
278}
279
280pub fn save_style(style: &str) -> bool {
282 let path = style_path();
283 if let Some(parent) = path.parent() {
284 let _ = fs::create_dir_all(parent);
285 }
286
287 let trimmed = style.trim();
288 if trimmed.is_empty() {
289 return match fs::remove_file(&path) {
290 Ok(_) => true,
291 Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
292 Err(e) => {
293 error!("❌ 删除 style.md 失败: {}", e);
294 false
295 }
296 };
297 }
298
299 match fs::write(path, trimmed) {
300 Ok(_) => true,
301 Err(e) => {
302 error!("❌ 保存 style.md 失败: {}", e);
303 false
304 }
305 }
306}