增加提示词管理
This commit is contained in:
@@ -373,6 +373,9 @@ pub fn init_database(app: &AppHandle) -> SqliteResult<Connection> {
|
||||
)?;
|
||||
}
|
||||
|
||||
// Initialize prompt files tables
|
||||
crate::commands::prompt_files::init_prompt_files_tables(&conn)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ pub mod git;
|
||||
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;
|
||||
|
||||
515
src-tauri/src/commands/prompt_files.rs
Normal file
515
src-tauri/src/commands/prompt_files.rs
Normal file
@@ -0,0 +1,515 @@
|
||||
use chrono::Utc;
|
||||
use dirs;
|
||||
use log::{error, info};
|
||||
use rusqlite::{params, Connection, Result as SqliteResult, Row};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{command, State};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands::agents::AgentDb;
|
||||
|
||||
/// 提示词文件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptFile {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub content: String,
|
||||
pub tags: Vec<String>,
|
||||
pub is_active: bool,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub last_used_at: Option<i64>,
|
||||
pub display_order: i32,
|
||||
}
|
||||
|
||||
/// 创建提示词文件请求
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreatePromptFileRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub content: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// 更新提示词文件请求
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UpdatePromptFileRequest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub content: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl PromptFile {
|
||||
pub fn from_row(row: &Row) -> Result<Self, rusqlite::Error> {
|
||||
let tags_str: String = row.get("tags")?;
|
||||
let tags: Vec<String> = serde_json::from_str(&tags_str).unwrap_or_default();
|
||||
|
||||
Ok(PromptFile {
|
||||
id: row.get("id")?,
|
||||
name: row.get("name")?,
|
||||
description: row.get("description")?,
|
||||
content: row.get("content")?,
|
||||
tags,
|
||||
is_active: row.get::<_, i32>("is_active")? == 1,
|
||||
created_at: row.get("created_at")?,
|
||||
updated_at: row.get("updated_at")?,
|
||||
last_used_at: row.get("last_used_at")?,
|
||||
display_order: row.get("display_order")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化提示词文件数据库表
|
||||
pub fn init_prompt_files_tables(conn: &Connection) -> SqliteResult<()> {
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS prompt_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
content TEXT NOT NULL,
|
||||
tags TEXT NOT NULL DEFAULT '[]',
|
||||
is_active INTEGER DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
last_used_at INTEGER,
|
||||
display_order INTEGER DEFAULT 0
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建索引
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_prompt_files_active ON prompt_files(is_active)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_prompt_files_name ON prompt_files(name)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
info!("Prompt files tables initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 列出所有提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_files_list(db: State<'_, AgentDb>) -> Result<Vec<PromptFile>, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, name, description, content, tags, is_active, created_at, updated_at,
|
||||
last_used_at, display_order
|
||||
FROM prompt_files
|
||||
ORDER BY display_order ASC, created_at DESC",
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let files = stmt
|
||||
.query_map([], |row| PromptFile::from_row(row))
|
||||
.map_err(|e| e.to_string())?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// 获取单个提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_file_get(id: String, db: State<'_, AgentDb>) -> Result<PromptFile, String> {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let file = conn
|
||||
.query_row(
|
||||
"SELECT id, name, description, content, tags, is_active, created_at, updated_at,
|
||||
last_used_at, display_order
|
||||
FROM prompt_files
|
||||
WHERE id = ?1",
|
||||
params![id],
|
||||
|row| PromptFile::from_row(row),
|
||||
)
|
||||
.map_err(|e| format!("提示词文件不存在: {}", e))?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// 创建提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_file_create(
|
||||
request: CreatePromptFileRequest,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<PromptFile, String> {
|
||||
info!("Creating prompt file: {}", request.name);
|
||||
|
||||
let id = {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// 检查名称是否已存在
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM prompt_files WHERE name = ?1",
|
||||
params![request.name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if exists {
|
||||
return Err(format!("提示词文件名称已存在: {}", request.name));
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now().timestamp();
|
||||
let tags_json = serde_json::to_string(&request.tags).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
// 获取当前最大 display_order
|
||||
let max_order: i32 = conn
|
||||
.query_row(
|
||||
"SELECT COALESCE(MAX(display_order), 0) FROM prompt_files",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO prompt_files
|
||||
(id, name, description, content, tags, is_active, created_at, updated_at, display_order)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, 0, ?6, ?6, ?7)",
|
||||
params![
|
||||
id.clone(),
|
||||
request.name,
|
||||
request.description,
|
||||
request.content,
|
||||
tags_json,
|
||||
now,
|
||||
max_order + 1
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("创建提示词文件失败: {}", e))?;
|
||||
|
||||
id
|
||||
}; // conn is dropped here
|
||||
|
||||
prompt_file_get(id, db).await
|
||||
}
|
||||
|
||||
/// 更新提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_file_update(
|
||||
request: UpdatePromptFileRequest,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<PromptFile, String> {
|
||||
info!("Updating prompt file: {}", request.id);
|
||||
|
||||
let id = {
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// 检查文件是否存在
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM prompt_files WHERE id = ?1",
|
||||
params![request.id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if !exists {
|
||||
return Err("提示词文件不存在".to_string());
|
||||
}
|
||||
|
||||
// 检查名称冲突(排除自己)
|
||||
let name_conflict: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM prompt_files WHERE name = ?1 AND id != ?2",
|
||||
params![request.name, request.id],
|
||||
|row| {
|
||||
let count: i32 = row.get(0)?;
|
||||
Ok(count > 0)
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if name_conflict {
|
||||
return Err(format!("提示词文件名称已存在: {}", request.name));
|
||||
}
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
let tags_json = serde_json::to_string(&request.tags).unwrap_or_else(|_| "[]".to_string());
|
||||
|
||||
conn.execute(
|
||||
"UPDATE prompt_files
|
||||
SET name = ?1, description = ?2, content = ?3, tags = ?4, updated_at = ?5
|
||||
WHERE id = ?6",
|
||||
params![
|
||||
request.name,
|
||||
request.description,
|
||||
request.content,
|
||||
tags_json,
|
||||
now,
|
||||
request.id.clone()
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("更新提示词文件失败: {}", e))?;
|
||||
|
||||
request.id
|
||||
}; // conn is dropped here
|
||||
|
||||
prompt_file_get(id, db).await
|
||||
}
|
||||
|
||||
/// 删除提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_file_delete(id: String, db: State<'_, AgentDb>) -> Result<(), String> {
|
||||
info!("Deleting prompt file: {}", id);
|
||||
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
let deleted = conn
|
||||
.execute("DELETE FROM prompt_files WHERE id = ?1", params![id])
|
||||
.map_err(|e| format!("删除提示词文件失败: {}", e))?;
|
||||
|
||||
if deleted == 0 {
|
||||
return Err("提示词文件不存在".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取 Claude 配置目录路径(~/.claude)
|
||||
fn get_claude_config_dir() -> Result<PathBuf, String> {
|
||||
let home_dir = dirs::home_dir()
|
||||
.ok_or_else(|| "无法获取主目录".to_string())?;
|
||||
|
||||
let claude_dir = home_dir.join(".claude");
|
||||
|
||||
// 确保目录存在
|
||||
if !claude_dir.exists() {
|
||||
fs::create_dir_all(&claude_dir)
|
||||
.map_err(|e| format!("创建 .claude 目录失败: {}", e))?;
|
||||
info!("创建 Claude 配置目录: {:?}", claude_dir);
|
||||
}
|
||||
|
||||
Ok(claude_dir)
|
||||
}
|
||||
|
||||
/// 应用提示词文件(替换本地 CLAUDE.md)
|
||||
#[command]
|
||||
pub async fn prompt_file_apply(
|
||||
id: String,
|
||||
target_path: Option<String>,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<String, String> {
|
||||
info!("Applying prompt file: {} to {:?}", id, target_path);
|
||||
|
||||
// 1. 从数据库读取提示词文件
|
||||
let file = prompt_file_get(id.clone(), db.clone()).await?;
|
||||
|
||||
// 2. 确定目标路径
|
||||
let claude_md_path = if let Some(path) = target_path {
|
||||
PathBuf::from(path).join("CLAUDE.md")
|
||||
} else {
|
||||
// 默认使用 ~/.claude/CLAUDE.md(和 settings.json 同目录)
|
||||
get_claude_config_dir()?.join("CLAUDE.md")
|
||||
};
|
||||
|
||||
// 3. 备份现有文件(如果存在)- 使用时间戳避免触发文件监视
|
||||
if claude_md_path.exists() {
|
||||
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
|
||||
let backup_path = claude_md_path.with_file_name(format!("CLAUDE.md.backup.{}", timestamp));
|
||||
fs::copy(&claude_md_path, &backup_path)
|
||||
.map_err(|e| format!("备份文件失败: {}", e))?;
|
||||
info!("Backed up existing CLAUDE.md to {:?}", backup_path);
|
||||
}
|
||||
|
||||
// 4. 写入新内容
|
||||
fs::write(&claude_md_path, &file.content)
|
||||
.map_err(|e| format!("写入文件失败: {}", e))?;
|
||||
|
||||
// 5. 更新数据库状态
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
// 将所有文件的 is_active 设为 0
|
||||
conn.execute("UPDATE prompt_files SET is_active = 0", [])
|
||||
.map_err(|e| format!("更新激活状态失败: {}", e))?;
|
||||
|
||||
// 将当前文件设为激活并更新最后使用时间
|
||||
let now = Utc::now().timestamp();
|
||||
conn.execute(
|
||||
"UPDATE prompt_files SET is_active = 1, last_used_at = ?1 WHERE id = ?2",
|
||||
params![now, id],
|
||||
)
|
||||
.map_err(|e| format!("更新激活状态失败: {}", e))?;
|
||||
|
||||
info!("Applied prompt file to {:?}", claude_md_path);
|
||||
Ok(claude_md_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// 取消使用当前提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_file_deactivate(db: State<'_, AgentDb>) -> Result<(), String> {
|
||||
info!("Deactivating all prompt files");
|
||||
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
conn.execute("UPDATE prompt_files SET is_active = 0", [])
|
||||
.map_err(|e| format!("取消激活失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从当前 CLAUDE.md 导入
|
||||
#[command]
|
||||
pub async fn prompt_file_import_from_claude_md(
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
source_path: Option<String>,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<PromptFile, String> {
|
||||
info!("Importing from CLAUDE.md: {:?}", source_path);
|
||||
|
||||
// 1. 确定源文件路径
|
||||
let claude_md_path = if let Some(path) = source_path {
|
||||
PathBuf::from(path)
|
||||
} else {
|
||||
// 默认从 ~/.claude/CLAUDE.md 导入
|
||||
get_claude_config_dir()?.join("CLAUDE.md")
|
||||
};
|
||||
|
||||
// 2. 读取文件内容
|
||||
if !claude_md_path.exists() {
|
||||
return Err("CLAUDE.md 文件不存在".to_string());
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&claude_md_path)
|
||||
.map_err(|e| format!("读取文件失败: {}", e))?;
|
||||
|
||||
// 3. 自动提取标签(简单实现:从内容中提取关键词)
|
||||
let tags = extract_tags_from_content(&content);
|
||||
|
||||
// 4. 创建提示词文件
|
||||
let request = CreatePromptFileRequest {
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
tags,
|
||||
};
|
||||
|
||||
prompt_file_create(request, db).await
|
||||
}
|
||||
|
||||
/// 从内容中提取标签(简单实现)
|
||||
fn extract_tags_from_content(content: &str) -> Vec<String> {
|
||||
let mut tags = Vec::new();
|
||||
let content_lower = content.to_lowercase();
|
||||
|
||||
// 常见技术栈关键词
|
||||
let keywords = [
|
||||
"react",
|
||||
"vue",
|
||||
"angular",
|
||||
"typescript",
|
||||
"javascript",
|
||||
"node",
|
||||
"nodejs",
|
||||
"express",
|
||||
"nest",
|
||||
"python",
|
||||
"django",
|
||||
"flask",
|
||||
"rust",
|
||||
"go",
|
||||
"java",
|
||||
"spring",
|
||||
"frontend",
|
||||
"backend",
|
||||
"fullstack",
|
||||
"api",
|
||||
"rest",
|
||||
"graphql",
|
||||
"database",
|
||||
"mongodb",
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"redis",
|
||||
"docker",
|
||||
"kubernetes",
|
||||
"aws",
|
||||
"testing",
|
||||
"jest",
|
||||
"vitest",
|
||||
];
|
||||
|
||||
for keyword in keywords.iter() {
|
||||
if content_lower.contains(keyword) {
|
||||
tags.push(keyword.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 限制标签数量
|
||||
tags.truncate(10);
|
||||
tags
|
||||
}
|
||||
|
||||
/// 导出提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_file_export(
|
||||
id: String,
|
||||
export_path: String,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<(), String> {
|
||||
info!("Exporting prompt file {} to {}", id, export_path);
|
||||
|
||||
let file = prompt_file_get(id, db).await?;
|
||||
|
||||
fs::write(&export_path, &file.content)
|
||||
.map_err(|e| format!("导出文件失败: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新提示词文件排序
|
||||
#[command]
|
||||
pub async fn prompt_files_update_order(
|
||||
ids: Vec<String>,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<(), String> {
|
||||
info!("Updating prompt files order");
|
||||
|
||||
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
for (index, id) in ids.iter().enumerate() {
|
||||
conn.execute(
|
||||
"UPDATE prompt_files SET display_order = ?1 WHERE id = ?2",
|
||||
params![index as i32, id],
|
||||
)
|
||||
.map_err(|e| format!("更新排序失败: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 批量导入提示词文件
|
||||
#[command]
|
||||
pub async fn prompt_files_import_batch(
|
||||
files: Vec<CreatePromptFileRequest>,
|
||||
db: State<'_, AgentDb>,
|
||||
) -> Result<Vec<PromptFile>, String> {
|
||||
info!("Batch importing {} prompt files", files.len());
|
||||
|
||||
let mut imported = Vec::new();
|
||||
|
||||
for request in files {
|
||||
match prompt_file_create(request, db.clone()).await {
|
||||
Ok(file) => imported.push(file),
|
||||
Err(e) => error!("Failed to import file: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
266
src-tauri/src/commands/prompt_files_v2.rs
Normal file
266
src-tauri/src/commands/prompt_files_v2.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
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<String>, // CLAUDE.md 文件内容
|
||||
pub file_size: Option<u64>, // 文件大小(字节)
|
||||
pub modified_at: Option<i64>, // 最后修改时间
|
||||
pub claude_md_path: String, // .claude/CLAUDE.md 完整路径
|
||||
}
|
||||
|
||||
/// 获取 Claude 目录
|
||||
fn get_claude_dir() -> Result<PathBuf, String> {
|
||||
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<String, String> {
|
||||
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::<serde_json::Value>(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<Vec<PromptFileInfo>, 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<String, String> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ use commands::ccr::{
|
||||
check_ccr_installation, get_ccr_config_path, get_ccr_service_status, get_ccr_version,
|
||||
open_ccr_ui, restart_ccr_service, start_ccr_service, stop_ccr_service,
|
||||
};
|
||||
use commands::prompt_files::{
|
||||
prompt_file_apply, prompt_file_create, prompt_file_deactivate, prompt_file_delete,
|
||||
prompt_file_export, prompt_file_get, prompt_file_import_from_claude_md,
|
||||
prompt_file_update, prompt_files_import_batch, prompt_files_list,
|
||||
prompt_files_update_order,
|
||||
};
|
||||
use commands::filesystem::{
|
||||
get_file_info, get_file_tree, get_watched_paths, read_directory_tree, read_file,
|
||||
search_files_by_name, unwatch_directory, watch_directory, write_file,
|
||||
@@ -446,6 +452,18 @@ fn main() {
|
||||
commands::slash_commands::slash_command_get,
|
||||
commands::slash_commands::slash_command_save,
|
||||
commands::slash_commands::slash_command_delete,
|
||||
// Prompt Files Management (Database Based)
|
||||
prompt_files_list,
|
||||
prompt_file_get,
|
||||
prompt_file_create,
|
||||
prompt_file_update,
|
||||
prompt_file_delete,
|
||||
prompt_file_apply,
|
||||
prompt_file_deactivate,
|
||||
prompt_file_import_from_claude_md,
|
||||
prompt_file_export,
|
||||
prompt_files_update_order,
|
||||
prompt_files_import_batch,
|
||||
// Proxy Settings
|
||||
get_proxy_settings,
|
||||
save_proxy_settings,
|
||||
|
||||
Reference in New Issue
Block a user