Files
CC-Switch/src-tauri/src/services/prompt.rs
T
Jason 1ee1e9cb2e refactor(startup): improve first-launch import logic with per-table detection
- Replace global `is_empty_for_first_import()` with independent checks:
  - `is_mcp_table_empty()` for MCP server imports
  - `is_prompts_table_empty()` for prompt imports
  - Skills and providers already have built-in idempotency checks

- Fix misleading logs in provider import:
  - Change `import_default_config` return type from `Result<()>` to `Result<bool>`
  - Return `true` when actually imported, `false` when skipped
  - Only log success message when import actually occurred

- Add idempotency protection to `import_from_file_on_first_launch`

This allows each data type to be independently recovered if deleted,
rather than requiring all tables to be empty for any import to trigger.
2025-11-28 12:05:29 +08:00

231 lines
8.2 KiB
Rust

use indexmap::IndexMap;
use crate::app_config::AppType;
use crate::config::write_text_file;
use crate::error::AppError;
use crate::prompt::Prompt;
use crate::prompt_files::prompt_file_path;
use crate::store::AppState;
/// 安全地获取当前 Unix 时间戳
fn get_unix_timestamp() -> Result<i64, AppError> {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.map_err(|e| AppError::Message(format!("Failed to get system time: {e}")))
}
pub struct PromptService;
impl PromptService {
pub fn get_prompts(
state: &AppState,
app: AppType,
) -> Result<IndexMap<String, Prompt>, AppError> {
state.db.get_prompts(app.as_str())
}
pub fn upsert_prompt(
state: &AppState,
app: AppType,
_id: &str,
prompt: Prompt,
) -> Result<(), AppError> {
// 检查是否为已启用的提示词
let is_enabled = prompt.enabled;
state.db.save_prompt(app.as_str(), &prompt)?;
// 如果是已启用的提示词,同步更新到对应的文件
if is_enabled {
let target_path = prompt_file_path(&app)?;
write_text_file(&target_path, &prompt.content)?;
}
Ok(())
}
pub fn delete_prompt(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
let prompts = state.db.get_prompts(app.as_str())?;
if let Some(prompt) = prompts.get(id) {
if prompt.enabled {
return Err(AppError::InvalidInput("无法删除已启用的提示词".to_string()));
}
}
state.db.delete_prompt(app.as_str(), id)?;
Ok(())
}
pub fn enable_prompt(state: &AppState, app: AppType, id: &str) -> Result<(), AppError> {
// 回填当前 live 文件内容到已启用的提示词,或创建备份
let target_path = prompt_file_path(&app)?;
if target_path.exists() {
if let Ok(live_content) = std::fs::read_to_string(&target_path) {
if !live_content.trim().is_empty() {
let mut prompts = state.db.get_prompts(app.as_str())?;
// 尝试回填到当前已启用的提示词
if let Some((enabled_id, enabled_prompt)) = prompts
.iter_mut()
.find(|(_, p)| p.enabled)
.map(|(id, p)| (id.clone(), p))
{
let timestamp = get_unix_timestamp()?;
enabled_prompt.content = live_content.clone();
enabled_prompt.updated_at = Some(timestamp);
log::info!("回填 live 提示词内容到已启用项: {enabled_id}");
state.db.save_prompt(app.as_str(), enabled_prompt)?;
} else {
// 没有已启用的提示词,则创建一次备份(避免重复备份)
let content_exists = prompts
.values()
.any(|p| p.content.trim() == live_content.trim());
if !content_exists {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let backup_id = format!("backup-{timestamp}");
let backup_prompt = Prompt {
id: backup_id.clone(),
name: format!(
"原始提示词 {}",
chrono::Local::now().format("%Y-%m-%d %H:%M")
),
content: live_content,
description: Some("自动备份的原始提示词".to_string()),
enabled: false,
created_at: Some(timestamp),
updated_at: Some(timestamp),
};
log::info!("回填 live 提示词内容,创建备份: {backup_id}");
state.db.save_prompt(app.as_str(), &backup_prompt)?;
}
}
}
}
}
// 启用目标提示词并写入文件
let mut prompts = state.db.get_prompts(app.as_str())?;
for prompt in prompts.values_mut() {
prompt.enabled = false;
}
if let Some(prompt) = prompts.get_mut(id) {
prompt.enabled = true;
write_text_file(&target_path, &prompt.content)?; // 原子写入
state.db.save_prompt(app.as_str(), prompt)?;
} else {
return Err(AppError::InvalidInput(format!("提示词 {id} 不存在")));
}
// Save all prompts to disable others
for (_, prompt) in prompts.iter() {
state.db.save_prompt(app.as_str(), prompt)?;
}
Ok(())
}
pub fn import_from_file(state: &AppState, app: AppType) -> Result<String, AppError> {
let file_path = prompt_file_path(&app)?;
if !file_path.exists() {
return Err(AppError::Message("提示词文件不存在".to_string()));
}
let content =
std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?;
let timestamp = get_unix_timestamp()?;
let id = format!("imported-{timestamp}");
let prompt = Prompt {
id: id.clone(),
name: format!(
"导入的提示词 {}",
chrono::Local::now().format("%Y-%m-%d %H:%M")
),
content,
description: Some("从现有配置文件导入".to_string()),
enabled: false,
created_at: Some(timestamp),
updated_at: Some(timestamp),
};
Self::upsert_prompt(state, app, &id, prompt)?;
Ok(id)
}
pub fn get_current_file_content(app: AppType) -> Result<Option<String>, AppError> {
let file_path = prompt_file_path(&app)?;
if !file_path.exists() {
return Ok(None);
}
let content =
std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?;
Ok(Some(content))
}
/// 首次启动时从现有提示词文件自动导入(如果存在)
/// 返回导入的数量
pub fn import_from_file_on_first_launch(
state: &AppState,
app: AppType,
) -> Result<usize, AppError> {
// 幂等性保护:该应用已有提示词则跳过
let existing = state.db.get_prompts(app.as_str())?;
if !existing.is_empty() {
return Ok(0);
}
let file_path = prompt_file_path(&app)?;
// 检查文件是否存在
if !file_path.exists() {
return Ok(0);
}
// 读取文件内容
let content = match std::fs::read_to_string(&file_path) {
Ok(c) => c,
Err(e) => {
log::warn!("读取提示词文件失败: {file_path:?}, 错误: {e}");
return Ok(0);
}
};
// 检查内容是否为空
if content.trim().is_empty() {
return Ok(0);
}
log::info!("发现提示词文件,自动导入: {file_path:?}");
// 创建提示词对象
let timestamp = get_unix_timestamp()?;
let id = format!("auto-imported-{timestamp}");
let prompt = Prompt {
id: id.clone(),
name: format!(
"Auto-imported Prompt {}",
chrono::Local::now().format("%Y-%m-%d %H:%M")
),
content,
description: Some("Automatically imported on first launch".to_string()),
enabled: true, // 首次导入时自动启用
created_at: Some(timestamp),
updated_at: Some(timestamp),
};
// 保存到数据库
state.db.save_prompt(app.as_str(), &prompt)?;
log::info!("自动导入完成: {}", app.as_str());
Ok(1)
}
}