Skip to main content

aster/plan/
persistence.rs

1//! Plan 持久化管理器
2//!
3//! 负责保存、加载、管理计划
4
5use std::fs;
6use std::path::PathBuf;
7use std::time::{SystemTime, UNIX_EPOCH};
8use uuid::Uuid;
9
10use super::types::*;
11
12/// 计划存储目录
13fn get_plans_dir() -> PathBuf {
14    dirs::home_dir()
15        .unwrap_or_else(|| PathBuf::from("."))
16        .join(".aster")
17        .join("plans")
18}
19
20/// 模板存储目录
21fn get_templates_dir() -> PathBuf {
22    dirs::home_dir()
23        .unwrap_or_else(|| PathBuf::from("."))
24        .join(".aster")
25        .join("plan-templates")
26}
27
28/// 版本存储目录
29fn get_versions_dir() -> PathBuf {
30    dirs::home_dir()
31        .unwrap_or_else(|| PathBuf::from("."))
32        .join(".aster")
33        .join("plan-versions")
34}
35
36/// 计划过期天数
37const PLAN_EXPIRY_DAYS: u64 = 90;
38
39/// Plan 持久化管理器
40pub struct PlanPersistenceManager;
41
42impl PlanPersistenceManager {
43    /// 确保目录存在
44    fn ensure_dirs() {
45        for dir in [get_plans_dir(), get_templates_dir(), get_versions_dir()] {
46            if !dir.exists() {
47                let _ = fs::create_dir_all(&dir);
48            }
49        }
50    }
51
52    /// 生成计划 ID
53    pub fn generate_plan_id() -> String {
54        let timestamp = SystemTime::now()
55            .duration_since(UNIX_EPOCH)
56            .unwrap_or_default()
57            .as_millis();
58        let uuid_str = Uuid::new_v4().to_string();
59        let random = uuid_str.get(..8).unwrap_or(&uuid_str);
60        format!("plan-{:x}-{}", timestamp, random)
61    }
62
63    /// 获取计划文件路径
64    fn get_plan_file_path(id: &str) -> PathBuf {
65        get_plans_dir().join(format!("{}.json", id))
66    }
67
68    /// 获取版本文件路径
69    fn get_version_file_path(plan_id: &str, version: u32) -> PathBuf {
70        get_versions_dir().join(format!("{}-v{}.json", plan_id, version))
71    }
72
73    /// 保存计划
74    pub fn save_plan(plan: &mut SavedPlan, create_version: bool) -> Result<(), String> {
75        Self::ensure_dirs();
76
77        let now = current_timestamp();
78        plan.metadata.updated_at = now;
79
80        if plan.metadata.created_at == 0 {
81            plan.metadata.created_at = now;
82            plan.metadata.version = 1;
83        }
84
85        let file_path = Self::get_plan_file_path(&plan.metadata.id);
86
87        // 如果需要创建版本,先保存旧版本
88        if create_version && file_path.exists() {
89            if let Ok(old_plan) = Self::load_plan(&plan.metadata.id) {
90                let _ = Self::save_version(&old_plan);
91                plan.metadata.version = old_plan.metadata.version + 1;
92            }
93        }
94
95        let data = serde_json::to_string_pretty(plan)
96            .map_err(|e| format!("Failed to serialize plan: {}", e))?;
97
98        fs::write(&file_path, data).map_err(|e| format!("Failed to write plan file: {}", e))?;
99
100        Ok(())
101    }
102
103    /// 加载计划
104    pub fn load_plan(id: &str) -> Result<SavedPlan, String> {
105        let file_path = Self::get_plan_file_path(id);
106
107        if !file_path.exists() {
108            return Err(format!("Plan not found: {}", id));
109        }
110
111        let data = fs::read_to_string(&file_path)
112            .map_err(|e| format!("Failed to read plan file: {}", e))?;
113
114        let plan: SavedPlan =
115            serde_json::from_str(&data).map_err(|e| format!("Failed to parse plan: {}", e))?;
116
117        if Self::is_expired(&plan) {
118            return Err("Plan has expired".to_string());
119        }
120
121        Ok(plan)
122    }
123
124    /// 删除计划
125    pub fn delete_plan(id: &str, delete_versions: bool) -> Result<(), String> {
126        let file_path = Self::get_plan_file_path(id);
127
128        if file_path.exists() {
129            fs::remove_file(&file_path).map_err(|e| format!("Failed to delete plan: {}", e))?;
130        }
131
132        if delete_versions {
133            if let Ok(versions) = Self::list_versions(id) {
134                for version in versions {
135                    let version_path = Self::get_version_file_path(id, version.version);
136                    let _ = fs::remove_file(version_path);
137                }
138            }
139        }
140
141        Ok(())
142    }
143
144    /// 列出所有计划
145    pub fn list_plans(options: &PlanListOptions) -> Vec<SavedPlan> {
146        Self::ensure_dirs();
147
148        let plans_dir = get_plans_dir();
149        let mut plans = Vec::new();
150
151        if let Ok(entries) = fs::read_dir(&plans_dir) {
152            for entry in entries.flatten() {
153                let path = entry.path();
154                if path.extension().map(|e| e == "json").unwrap_or(false) {
155                    if let Some(id) = path.file_stem().and_then(|s| s.to_str()) {
156                        if let Ok(plan) = Self::load_plan(id) {
157                            plans.push(plan);
158                        }
159                    }
160                }
161            }
162        }
163
164        // 应用过滤和排序
165        plans = Self::apply_filters(plans, options);
166        plans = Self::apply_sorting(plans, options);
167
168        // 应用分页
169        let offset = options.offset.unwrap_or(0);
170        let limit = options.limit.unwrap_or(plans.len());
171        plans.into_iter().skip(offset).take(limit).collect()
172    }
173
174    /// 应用过滤器
175    fn apply_filters(mut plans: Vec<SavedPlan>, options: &PlanListOptions) -> Vec<SavedPlan> {
176        // 搜索过滤
177        if let Some(ref search) = options.search {
178            let search_lower = search.to_lowercase();
179            plans.retain(|p| {
180                p.metadata.title.to_lowercase().contains(&search_lower)
181                    || p.metadata
182                        .description
183                        .to_lowercase()
184                        .contains(&search_lower)
185                    || p.summary.to_lowercase().contains(&search_lower)
186            });
187        }
188
189        // 标签过滤
190        if let Some(ref tags) = options.tags {
191            plans.retain(|p| {
192                p.metadata
193                    .tags
194                    .as_ref()
195                    .is_some_and(|plan_tags| tags.iter().any(|t| plan_tags.contains(t)))
196            });
197        }
198
199        // 状态过滤
200        if let Some(ref statuses) = options.status {
201            plans.retain(|p| statuses.contains(&p.metadata.status));
202        }
203
204        // 优先级过滤
205        if let Some(ref priorities) = options.priority {
206            plans.retain(|p| {
207                p.metadata
208                    .priority
209                    .as_ref()
210                    .is_some_and(|pr| priorities.contains(pr))
211            });
212        }
213
214        // 工作目录过滤
215        if let Some(ref wd) = options.working_directory {
216            plans.retain(|p| p.metadata.working_directory.starts_with(wd));
217        }
218
219        plans
220    }
221
222    /// 应用排序
223    fn apply_sorting(mut plans: Vec<SavedPlan>, options: &PlanListOptions) -> Vec<SavedPlan> {
224        let sort_by = options.sort_by.unwrap_or(SortField::UpdatedAt);
225        let sort_order = options.sort_order.unwrap_or(SortOrder::Desc);
226
227        plans.sort_by(|a, b| {
228            let cmp = match sort_by {
229                SortField::CreatedAt => a.metadata.created_at.cmp(&b.metadata.created_at),
230                SortField::UpdatedAt => a.metadata.updated_at.cmp(&b.metadata.updated_at),
231                SortField::Title => a.metadata.title.cmp(&b.metadata.title),
232                SortField::Priority => {
233                    let pa = priority_to_num(a.metadata.priority.as_ref());
234                    let pb = priority_to_num(b.metadata.priority.as_ref());
235                    pa.cmp(&pb)
236                }
237                SortField::Status => {
238                    format!("{:?}", a.metadata.status).cmp(&format!("{:?}", b.metadata.status))
239                }
240            };
241
242            match sort_order {
243                SortOrder::Asc => cmp,
244                SortOrder::Desc => cmp.reverse(),
245            }
246        });
247
248        plans
249    }
250
251    /// 检查计划是否过期
252    fn is_expired(plan: &SavedPlan) -> bool {
253        let now = current_timestamp();
254        let age_ms = now.saturating_sub(plan.metadata.created_at);
255        let expiry_ms = PLAN_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
256        age_ms > expiry_ms
257    }
258
259    /// 保存版本
260    pub fn save_version(plan: &SavedPlan) -> Result<(), String> {
261        Self::ensure_dirs();
262
263        let version = plan.metadata.version;
264        let version_path = Self::get_version_file_path(&plan.metadata.id, version);
265
266        let data = serde_json::to_string_pretty(plan)
267            .map_err(|e| format!("Failed to serialize version: {}", e))?;
268
269        fs::write(&version_path, data)
270            .map_err(|e| format!("Failed to write version file: {}", e))?;
271
272        Ok(())
273    }
274
275    /// 列出计划的所有版本
276    pub fn list_versions(plan_id: &str) -> Result<Vec<PlanVersion>, String> {
277        let versions_dir = get_versions_dir();
278        let mut versions = Vec::new();
279
280        let current_plan = Self::load_plan(plan_id).ok();
281        let current_version = current_plan
282            .as_ref()
283            .map(|p| p.metadata.version)
284            .unwrap_or(1);
285
286        if let Ok(entries) = fs::read_dir(&versions_dir) {
287            for entry in entries.flatten() {
288                let path = entry.path();
289                let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
290
291                if !filename.starts_with(plan_id) || !filename.ends_with(".json") {
292                    continue;
293                }
294
295                if let Some(version) = extract_version_number(filename) {
296                    let metadata = fs::metadata(&path).ok();
297                    let created_at = metadata
298                        .and_then(|m| m.modified().ok())
299                        .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
300                        .map(|d| d.as_millis() as u64)
301                        .unwrap_or(0);
302
303                    versions.push(PlanVersion {
304                        version,
305                        plan_id: plan_id.to_string(),
306                        created_at,
307                        change_summary: format!("Version {}", version),
308                        author: None,
309                        is_current: version == current_version,
310                    });
311                }
312            }
313        }
314
315        versions.sort_by(|a, b| b.version.cmp(&a.version));
316        Ok(versions)
317    }
318
319    /// 恢复到指定版本
320    pub fn restore_version(plan_id: &str, version: u32) -> Result<(), String> {
321        let version_path = Self::get_version_file_path(plan_id, version);
322
323        if !version_path.exists() {
324            return Err(format!("Version {} not found", version));
325        }
326
327        let data = fs::read_to_string(&version_path)
328            .map_err(|e| format!("Failed to read version: {}", e))?;
329
330        let mut plan: SavedPlan =
331            serde_json::from_str(&data).map_err(|e| format!("Failed to parse version: {}", e))?;
332
333        // 保存当前版本
334        if let Ok(current) = Self::load_plan(plan_id) {
335            let _ = Self::save_version(&current);
336        }
337
338        plan.metadata.updated_at = current_timestamp();
339        Self::save_plan(&mut plan, false)
340    }
341
342    /// 更新计划状态
343    pub fn update_plan_status(
344        id: &str,
345        status: PlanStatus,
346        approved_by: Option<&str>,
347        rejection_reason: Option<&str>,
348    ) -> Result<(), String> {
349        let mut plan = Self::load_plan(id)?;
350
351        plan.metadata.status = status;
352        plan.metadata.updated_at = current_timestamp();
353
354        if matches!(status, PlanStatus::Approved) {
355            if let Some(by) = approved_by {
356                plan.metadata.approved_by = Some(by.to_string());
357                plan.metadata.approved_at = Some(current_timestamp());
358            }
359        }
360
361        if matches!(status, PlanStatus::Rejected) {
362            if let Some(reason) = rejection_reason {
363                plan.metadata.rejection_reason = Some(reason.to_string());
364            }
365        }
366
367        if matches!(status, PlanStatus::Completed) {
368            plan.completed_at = Some(current_timestamp());
369        }
370
371        Self::save_plan(&mut plan, true)
372    }
373
374    /// 导出计划
375    pub fn export_plan(plan_id: &str, options: &PlanExportOptions) -> Result<String, String> {
376        let plan = Self::load_plan(plan_id)?;
377
378        match options.format {
379            ExportFormat::Json => Self::export_as_json(&plan, options),
380            ExportFormat::Markdown => Ok(Self::export_as_markdown(&plan, options)),
381            ExportFormat::Html => Ok(Self::export_as_html(&plan, options)),
382        }
383    }
384
385    fn export_as_json(plan: &SavedPlan, _options: &PlanExportOptions) -> Result<String, String> {
386        serde_json::to_string_pretty(plan).map_err(|e| format!("Failed to export as JSON: {}", e))
387    }
388
389    fn export_as_markdown(plan: &SavedPlan, options: &PlanExportOptions) -> String {
390        let mut lines = Vec::new();
391
392        lines.push(format!("# {}", plan.metadata.title));
393        lines.push(String::new());
394
395        if options.include_metadata {
396            lines.push("## Metadata".to_string());
397            lines.push(format!("- Status: {:?}", plan.metadata.status));
398            lines.push(format!("- Priority: {:?}", plan.metadata.priority));
399            lines.push(String::new());
400        }
401
402        lines.push("## Summary".to_string());
403        lines.push(plan.summary.clone());
404        lines.push(String::new());
405
406        lines.push("## Implementation Steps".to_string());
407        for step in &plan.steps {
408            lines.push(format!("### Step {}: {}", step.step, step.description));
409            lines.push(format!("- Complexity: {:?}", step.complexity));
410            lines.push(format!("- Files: {}", step.files.join(", ")));
411            lines.push(String::new());
412        }
413
414        if options.include_risks && !plan.risks.is_empty() {
415            lines.push("## Risks".to_string());
416            for risk in &plan.risks {
417                lines.push(format!("- **[{:?}]** {}", risk.level, risk.description));
418            }
419            lines.push(String::new());
420        }
421
422        lines.join("\n")
423    }
424
425    fn export_as_html(plan: &SavedPlan, options: &PlanExportOptions) -> String {
426        let markdown = Self::export_as_markdown(plan, options);
427        format!(
428            r#"<!DOCTYPE html>
429<html><head><title>{}</title></head>
430<body><pre>{}</pre></body></html>"#,
431            plan.metadata.title, markdown
432        )
433    }
434}
435
436// 辅助函数
437
438fn current_timestamp() -> u64 {
439    SystemTime::now()
440        .duration_since(UNIX_EPOCH)
441        .unwrap_or_default()
442        .as_millis() as u64
443}
444
445fn priority_to_num(priority: Option<&Priority>) -> u8 {
446    match priority {
447        Some(Priority::Low) => 1,
448        Some(Priority::Medium) => 2,
449        Some(Priority::High) => 3,
450        Some(Priority::Critical) => 4,
451        None => 0,
452    }
453}
454
455fn extract_version_number(filename: &str) -> Option<u32> {
456    let re = regex::Regex::new(r"-v(\d+)\.json$").ok()?;
457    let caps = re.captures(filename)?;
458    caps.get(1)?.as_str().parse().ok()
459}