use anyhow::Result; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use tauri::command; use log::{info, warn}; /// 提示词文件信息(从文件系统读取) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PromptFileInfo { pub project_id: String, // 项目 ID(来自 projects 目录名) pub project_path: String, // 实际项目路径 pub has_claude_md: bool, // 是否存在 .claude/CLAUDE.md pub content: Option, // CLAUDE.md 文件内容 pub file_size: Option, // 文件大小(字节) pub modified_at: Option, // 最后修改时间 pub claude_md_path: String, // .claude/CLAUDE.md 完整路径 } /// 获取 Claude 目录 fn get_claude_dir() -> Result { dirs::home_dir() .map(|p| p.join(".claude")) .ok_or_else(|| "无法获取 home 目录".to_string()) } /// 从项目名解码实际路径 fn decode_project_path(encoded_name: &str) -> String { // 简单的路径解码 - 将编码的斜杠 (%2F 或 -) 转回斜杠 encoded_name.replace("%2F", "/").replace("-", "/") } /// 从会话文件中获取项目路径 fn get_project_path_from_sessions(project_dir: &PathBuf) -> Result { let entries = fs::read_dir(project_dir) .map_err(|e| format!("无法读取项目目录: {}", e))?; for entry in entries.flatten() { let path = entry.path(); if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("jsonl") { // 读取 JSONL 文件的第一行 if let Ok(content) = fs::read_to_string(&path) { if let Some(first_line) = content.lines().next() { if let Ok(json) = serde_json::from_str::(first_line) { if let Some(cwd) = json.get("cwd").and_then(|v| v.as_str()) { return Ok(cwd.to_string()); } } } } } } Err("未找到项目路径".to_string()) } /// 扫描所有项目的提示词文件 #[command] pub async fn scan_prompt_files() -> Result, String> { info!("扫描项目提示词文件"); let claude_dir = get_claude_dir()?; let projects_dir = claude_dir.join("projects"); if !projects_dir.exists() { warn!("项目目录不存在: {:?}", projects_dir); return Ok(Vec::new()); } let mut prompt_files = Vec::new(); // 读取所有项目目录 let entries = fs::read_dir(&projects_dir) .map_err(|e| format!("无法读取项目目录: {}", e))?; for entry in entries { let entry = entry.map_err(|e| format!("无法读取目录条目: {}", e))?; let path = entry.path(); if path.is_dir() { let project_id = path .file_name() .and_then(|n| n.to_str()) .ok_or_else(|| "无效的目录名".to_string())? .to_string(); // 获取实际项目路径 let project_path = match get_project_path_from_sessions(&path) { Ok(p) => p.clone(), Err(_) => { warn!("无法从会话获取项目路径,使用解码: {}", project_id); decode_project_path(&project_id) } }; // 检查 .claude/CLAUDE.md 是否存在 let claude_md_path = PathBuf::from(&project_path).join(".claude").join("CLAUDE.md"); let has_claude_md = claude_md_path.exists(); let (content, file_size, modified_at) = if has_claude_md { // 读取文件内容 let content = fs::read_to_string(&claude_md_path) .ok(); // 获取文件元数据 let metadata = fs::metadata(&claude_md_path).ok(); let file_size = metadata.as_ref().map(|m| m.len()); let modified_at = metadata .and_then(|m| m.modified().ok()) .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) .map(|d| d.as_secs() as i64); (content, file_size, modified_at) } else { (None, None, None) }; prompt_files.push(PromptFileInfo { project_id, project_path: project_path.clone(), has_claude_md, content, file_size, modified_at, claude_md_path: claude_md_path.to_string_lossy().to_string(), }); } } // 按最后修改时间排序(最新的在前) prompt_files.sort_by(|a, b| { match (b.modified_at, a.modified_at) { (Some(b_time), Some(a_time)) => b_time.cmp(&a_time), (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => a.project_path.cmp(&b.project_path), } }); info!("找到 {} 个项目,其中 {} 个有 CLAUDE.md", prompt_files.len(), prompt_files.iter().filter(|p| p.has_claude_md).count() ); Ok(prompt_files) } /// 读取指定项目的 CLAUDE.md 文件 #[command] pub async fn read_prompt_file(project_path: String) -> Result { info!("读取提示词文件: {}", project_path); let claude_md_path = PathBuf::from(&project_path).join(".claude").join("CLAUDE.md"); if !claude_md_path.exists() { return Err(format!("文件不存在: {:?}", claude_md_path)); } fs::read_to_string(&claude_md_path) .map_err(|e| format!("读取文件失败: {}", e)) } /// 保存 CLAUDE.md 文件 #[command] pub async fn save_prompt_file(project_path: String, content: String) -> Result<(), String> { info!("保存提示词文件: {}", project_path); let claude_dir = PathBuf::from(&project_path).join(".claude"); let claude_md_path = claude_dir.join("CLAUDE.md"); // 确保 .claude 目录存在 if !claude_dir.exists() { fs::create_dir_all(&claude_dir) .map_err(|e| format!("创建 .claude 目录失败: {}", e))?; info!("创建 .claude 目录: {:?}", claude_dir); } // 备份现有文件 if claude_md_path.exists() { let backup_path = claude_md_path.with_extension("md.backup"); fs::copy(&claude_md_path, &backup_path) .map_err(|e| format!("备份文件失败: {}", e))?; info!("备份现有文件到: {:?}", backup_path); } // 写入新内容 fs::write(&claude_md_path, content) .map_err(|e| format!("写入文件失败: {}", e))?; info!("成功保存文件: {:?}", claude_md_path); Ok(()) } /// 创建新的 CLAUDE.md 文件 #[command] pub async fn create_prompt_file(project_path: String, content: String) -> Result<(), String> { info!("创建提示词文件: {}", project_path); let claude_dir = PathBuf::from(&project_path).join(".claude"); let claude_md_path = claude_dir.join("CLAUDE.md"); // 检查文件是否已存在 if claude_md_path.exists() { return Err("CLAUDE.md 文件已存在,请使用编辑功能".to_string()); } // 确保 .claude 目录存在 if !claude_dir.exists() { fs::create_dir_all(&claude_dir) .map_err(|e| format!("创建 .claude 目录失败: {}", e))?; info!("创建 .claude 目录: {:?}", claude_dir); } // 写入内容 fs::write(&claude_md_path, content) .map_err(|e| format!("写入文件失败: {}", e))?; info!("成功创建文件: {:?}", claude_md_path); Ok(()) } /// 删除 CLAUDE.md 文件 #[command] pub async fn delete_prompt_file(project_path: String) -> Result<(), String> { info!("删除提示词文件: {}", project_path); let claude_md_path = PathBuf::from(&project_path).join(".claude").join("CLAUDE.md"); if !claude_md_path.exists() { return Err("文件不存在".to_string()); } // 备份到 .backup let backup_path = claude_md_path.with_extension("md.backup"); fs::copy(&claude_md_path, &backup_path) .map_err(|e| format!("备份文件失败: {}", e))?; // 删除文件 fs::remove_file(&claude_md_path) .map_err(|e| format!("删除文件失败: {}", e))?; info!("成功删除文件(已备份到 {:?})", backup_path); Ok(()) } /// 复制 CLAUDE.md 到另一个项目 #[command] pub async fn copy_prompt_file( source_project_path: String, target_project_path: String, ) -> Result<(), String> { info!("复制提示词文件: {} -> {}", source_project_path, target_project_path); let source_path = PathBuf::from(&source_project_path).join(".claude").join("CLAUDE.md"); if !source_path.exists() { return Err("源文件不存在".to_string()); } // 读取源文件 let content = fs::read_to_string(&source_path) .map_err(|e| format!("读取源文件失败: {}", e))?; // 保存到目标路径 save_prompt_file(target_project_path, content).await }