mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
1ee1e9cb2e
- 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.
231 lines
8.2 KiB
Rust
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)
|
|
}
|
|
}
|