j_cli/command/chat/
archive.rs1use super::model::ChatMessage;
2use crate::error;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ChatArchive {
13 pub name: String,
15 pub created_at: String,
17 pub messages: Vec<ChatMessage>,
19}
20
21pub fn get_archives_dir() -> PathBuf {
25 super::model::agent_data_dir().join("archives")
26}
27
28pub fn ensure_archives_dir() -> std::io::Result<()> {
30 let dir = get_archives_dir();
31 if !dir.exists() {
32 fs::create_dir_all(&dir)?;
33 }
34 Ok(())
35}
36
37pub fn list_archives() -> Vec<ChatArchive> {
41 let dir = get_archives_dir();
42 if !dir.exists() {
43 return Vec::new();
44 }
45
46 let mut archives = Vec::new();
47
48 if let Ok(entries) = fs::read_dir(&dir) {
49 for entry in entries.flatten() {
50 let path = entry.path();
51 if path.extension().map_or(false, |ext| ext == "json") {
52 if let Ok(content) = fs::read_to_string(&path) {
53 match serde_json::from_str::<ChatArchive>(&content) {
54 Ok(archive) => archives.push(archive),
55 Err(e) => {
56 error!("[list_archives] 解析归档文件失败: {:?}, 错误: {}", path, e);
57 }
58 }
59 }
60 }
61 }
62 }
63
64 archives.sort_by(|a, b| b.created_at.cmp(&a.created_at));
66 archives
67}
68
69pub fn create_archive(name: &str, messages: Vec<ChatMessage>) -> Result<ChatArchive, String> {
71 validate_archive_name(name)?;
73
74 if let Err(e) = ensure_archives_dir() {
76 return Err(format!("创建归档目录失败: {}", e));
77 }
78
79 let now: DateTime<Utc> = Utc::now();
80 let archive = ChatArchive {
81 name: name.to_string(),
82 created_at: now.to_rfc3339(),
83 messages,
84 };
85
86 let path = get_archive_path(name);
87 let json =
88 serde_json::to_string_pretty(&archive).map_err(|e| format!("序列化归档失败: {}", e))?;
89
90 fs::write(&path, json).map_err(|e| format!("写入归档文件失败: {}", e))?;
91
92 Ok(archive)
93}
94
95pub fn restore_archive(name: &str) -> Result<Vec<ChatMessage>, String> {
97 let path = get_archive_path(name);
98
99 if !path.exists() {
100 return Err(format!("归档文件不存在: {}", name));
101 }
102
103 let content = fs::read_to_string(&path).map_err(|e| format!("读取归档文件失败: {}", e))?;
104
105 let archive: ChatArchive =
106 serde_json::from_str(&content).map_err(|e| format!("解析归档文件失败: {}", e))?;
107
108 Ok(archive.messages)
109}
110
111pub fn delete_archive(name: &str) -> Result<(), String> {
113 let path = get_archive_path(name);
114
115 if !path.exists() {
116 return Err(format!("归档文件不存在: {}", name));
117 }
118
119 fs::remove_file(&path).map_err(|e| format!("删除归档文件失败: {}", e))?;
120
121 Ok(())
122}
123
124pub fn validate_archive_name(name: &str) -> Result<(), String> {
126 if name.is_empty() {
128 return Err("归档名称不能为空".to_string());
129 }
130 if name.len() > 50 {
131 return Err("归档名称过长,最多 50 字符".to_string());
132 }
133
134 let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
136 for c in invalid_chars {
137 if name.contains(c) {
138 return Err(format!("归档名称包含非法字符: {}", c));
139 }
140 }
141
142 Ok(())
143}
144
145pub fn generate_default_archive_name() -> String {
147 let today = chrono::Local::now().format("%Y-%m-%d").to_string();
148 let base_name = format!("archive-{}", today);
149
150 if !archive_exists(&base_name) {
152 return base_name;
153 }
154
155 let mut suffix = 1;
157 loop {
158 let name = format!("{}({})", base_name, suffix);
159 if !archive_exists(&name) {
160 return name;
161 }
162 suffix += 1;
163 }
164}
165
166pub fn archive_exists(name: &str) -> bool {
170 let path = get_archives_dir().join(format!("{}.json", name));
171 path.exists()
172}
173
174fn get_archive_path(name: &str) -> PathBuf {
176 get_archives_dir().join(format!("{}.json", name))
177}