增加提示词管理

This commit is contained in:
2025-10-21 15:08:31 +08:00
parent 0e32c6e64c
commit 7021ab6bec
17 changed files with 2286 additions and 6 deletions

View File

@@ -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)
}

View File

@@ -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;

View 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)
}

View 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
}

View File

@@ -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,