diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 36ffc0b..2763bae 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -7,7 +7,6 @@ pub mod language; pub mod mcp; pub mod packycode_nodes; pub mod prompt_files; -pub mod prompt_files_v2; pub mod proxy; pub mod relay_adapters; pub mod relay_stations; diff --git a/src-tauri/src/commands/prompt_files.rs b/src-tauri/src/commands/prompt_files.rs index 7d2f4fa..c6cc3f2 100644 --- a/src-tauri/src/commands/prompt_files.rs +++ b/src-tauri/src/commands/prompt_files.rs @@ -297,7 +297,7 @@ fn get_claude_config_dir() -> Result { Ok(claude_dir) } -/// 应用提示词文件(替换本地 CLAUDE.md) +/// 应用提示词文件(替换本地 CLAUDE.md 或指定目标路径) #[command] pub async fn prompt_file_apply( id: String, @@ -309,14 +309,40 @@ pub async fn prompt_file_apply( // 1. 从数据库读取提示词文件 let file = prompt_file_get(id.clone(), db.clone()).await?; - // 2. 确定目标路径 + // 2. 确定目标路径(兼容传入目录或文件) + // - 若传入目录:拼接 CLAUDE.md + // - 若传入文件:直接写入该文件 + // - 若未传入:默认 ~/.claude/CLAUDE.md let claude_md_path = if let Some(path) = target_path { - PathBuf::from(path).join("CLAUDE.md") + let p = PathBuf::from(path); + let is_dir = p.is_dir(); + // 对于不存在路径,依据文件名扩展名进行语义判断 + let looks_like_file = p + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.eq_ignore_ascii_case("claude.md") || n.to_lowercase().ends_with(".md")) + .unwrap_or(false); + + if is_dir || (!looks_like_file && !p.exists()) { + // 目录(或看起来像目录的不存在路径) + p.join("CLAUDE.md") + } else { + // 明确的文件路径 + p + } } else { // 默认使用 ~/.claude/CLAUDE.md(和 settings.json 同目录) get_claude_config_dir()?.join("CLAUDE.md") }; + // 确保父目录存在 + if let Some(parent) = claude_md_path.parent() { + if !parent.exists() { + fs::create_dir_all(parent) + .map_err(|e| format!("创建目标目录失败: {}", e))?; + } + } + // 3. 备份现有文件(如果存在)- 使用时间戳避免触发文件监视 if claude_md_path.exists() { let timestamp = Utc::now().format("%Y%m%d_%H%M%S"); @@ -512,4 +538,3 @@ pub async fn prompt_files_import_batch( Ok(imported) } - diff --git a/src-tauri/src/commands/prompt_files_v2.rs b/src-tauri/src/commands/prompt_files_v2.rs deleted file mode 100644 index e5da746..0000000 --- a/src-tauri/src/commands/prompt_files_v2.rs +++ /dev/null @@ -1,266 +0,0 @@ -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 -} - diff --git a/src/components/PromptFilePreview.tsx b/src/components/PromptFilePreview.tsx index 38a112a..82beb22 100644 --- a/src/components/PromptFilePreview.tsx +++ b/src/components/PromptFilePreview.tsx @@ -1,4 +1,7 @@ import React from 'react'; +import { save } from '@tauri-apps/plugin-dialog'; +import { usePromptFilesStore } from '@/stores/promptFilesStore'; +import { useTranslation } from '@/hooks/useTranslation'; import { Edit, Play, Tag as TagIcon, Clock, Calendar } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -28,6 +31,8 @@ export const PromptFilePreview: React.FC = ({ onEdit, onApply, }) => { + const { applyFile } = usePromptFilesStore(); + const { t } = useTranslation(); const formatDate = (timestamp: number) => { return new Date(timestamp * 1000).toLocaleString('zh-CN', { year: 'numeric', @@ -38,6 +43,19 @@ export const PromptFilePreview: React.FC = ({ }); }; + const handleApplyToCustom = async () => { + const selectedPath = await save({ + defaultPath: 'CLAUDE.md', + filters: [ + { name: 'Markdown', extensions: ['md'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }); + if (!selectedPath) return; + await applyFile(file.id, String(selectedPath)); + onOpenChange(false); + }; + return ( @@ -100,6 +118,11 @@ export const PromptFilePreview: React.FC = ({ 使用此文件 )} + @@ -108,4 +131,3 @@ export const PromptFilePreview: React.FC = ({ }; export default PromptFilePreview; - diff --git a/src/components/PromptFilesManager.tsx b/src/components/PromptFilesManager.tsx index b7b6acd..15f5bf6 100644 --- a/src/components/PromptFilesManager.tsx +++ b/src/components/PromptFilesManager.tsx @@ -37,6 +37,7 @@ import type { PromptFile } from '@/lib/api'; import { cn } from '@/lib/utils'; import { PromptFileEditor } from './PromptFileEditor'; import { PromptFilePreview } from './PromptFilePreview'; +import { save } from '@tauri-apps/plugin-dialog'; interface PromptFilesManagerProps { onBack?: () => void; @@ -96,6 +97,29 @@ export const PromptFilesManager: React.FC = ({ onBack, } }; + // 应用到自定义路径(文件路径),跨平台 + const handleApplyToCustom = async (file: PromptFile) => { + try { + const selectedPath = await save({ + defaultPath: 'CLAUDE.md', + filters: [ + { name: 'Markdown', extensions: ['md'] }, + { name: 'All Files', extensions: ['*'] }, + ], + }); + if (!selectedPath) return; // 用户取消 + + setApplyingFileId(file.id); + const resultPath = await applyFile(file.id, String(selectedPath)); + showToast(`已应用到: ${resultPath}`, 'success'); + await loadFiles(); + } catch (error) { + showToast(t('promptFiles.applyToCustomPathFailed'), 'error'); + } finally { + setApplyingFileId(null); + } + }; + const handleDeactivate = async () => { try { await deactivateAll(); @@ -183,8 +207,8 @@ export const PromptFilesManager: React.FC = ({ onBack, )}
-

提示词文件管理

-

管理和切换 Claude 项目提示词文件

+

{t('promptFiles.title')}

+

{t('promptFiles.description')}

@@ -289,6 +313,15 @@ export const PromptFilesManager: React.FC = ({ onBack, )} + +