Skip to main content

j_cli/command/chat/
archive.rs

1use super::model::ChatMessage;
2use crate::error;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7
8// ========== 数据结构 ==========
9
10/// 归档数据结构
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ChatArchive {
13    /// 归档名称
14    pub name: String,
15    /// 创建时间(ISO 8601 格式)
16    pub created_at: String,
17    /// 消息列表
18    pub messages: Vec<ChatMessage>,
19}
20
21// ========== 文件路径 ==========
22
23/// 获取归档目录路径: ~/.jdata/agent/data/archives/
24pub fn get_archives_dir() -> PathBuf {
25    super::model::agent_data_dir().join("archives")
26}
27
28/// 确保归档目录存在
29pub 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
37// ========== 归档操作 ==========
38
39/// 列出所有归档文件
40pub 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    // 按创建时间倒序排列
65    archives.sort_by(|a, b| b.created_at.cmp(&a.created_at));
66    archives
67}
68
69/// 创建新归档
70pub fn create_archive(name: &str, messages: Vec<ChatMessage>) -> Result<ChatArchive, String> {
71    // 校验名称
72    validate_archive_name(name)?;
73
74    // 确保归档目录存在
75    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
95/// 从归档恢复消息
96pub 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
111/// 删除归档
112pub 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
124/// 校验归档名称合法性
125pub fn validate_archive_name(name: &str) -> Result<(), String> {
126    // 检查名称长度
127    if name.is_empty() {
128        return Err("归档名称不能为空".to_string());
129    }
130    if name.len() > 50 {
131        return Err("归档名称过长,最多 50 字符".to_string());
132    }
133
134    // 检查非法字符
135    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
145/// 生成默认归档名称(格式:archive-YYYY-MM-DD,重名时自动添加后缀)
146pub 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    // 如果基础名称不存在,直接使用
151    if !archive_exists(&base_name) {
152        return base_name;
153    }
154
155    // 重名时添加后缀 (1), (2), ...
156    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
166// ========== 辅助函数 ==========
167
168/// 检查归档是否存在
169pub fn archive_exists(name: &str) -> bool {
170    let path = get_archives_dir().join(format!("{}.json", name));
171    path.exists()
172}
173
174/// 获取归档文件路径
175fn get_archive_path(name: &str) -> PathBuf {
176    get_archives_dir().join(format!("{}.json", name))
177}