fix(prompt): improve i18n and error handling for auto-import

Address code review feedback for PR #214:

- Replace hardcoded Chinese strings with English in auto-imported prompts
  - Prompt name: "Auto-imported Prompt" instead of "初始提示词"
  - Description: "Automatically imported on first launch"
- Remove panic risk by replacing expect() with proper error propagation
- Use AppError::localized for bilingual error messages
- Extract get_base_dir_with_fallback() helper to eliminate code duplication
- Update test assertions to match new English strings
- Suppress false-positive dead_code warning on TempHome.dir field

All 5 tests passing with zero compiler warnings.
This commit is contained in:
Jason
2025-11-13 15:43:37 +08:00
parent e4d7999294
commit 30c763ffe3
4 changed files with 81 additions and 45 deletions

View File

@@ -234,10 +234,7 @@ impl MultiAppConfig {
} }
/// 检查并自动导入单个应用的提示词文件 /// 检查并自动导入单个应用的提示词文件
fn auto_import_prompt_if_exists( fn auto_import_prompt_if_exists(config: &mut Self, app: AppType) -> Result<(), AppError> {
config: &mut Self,
app: AppType,
) -> Result<(), AppError> {
let file_path = prompt_file_path(&app)?; let file_path = prompt_file_path(&app)?;
// 检查文件是否存在 // 检查文件是否存在
@@ -273,11 +270,11 @@ impl MultiAppConfig {
let prompt = crate::prompt::Prompt { let prompt = crate::prompt::Prompt {
id: id.clone(), id: id.clone(),
name: format!( name: format!(
"初始提示词 {}", "Auto-imported Prompt {}",
chrono::Local::now().format("%Y-%m-%d %H:%M") chrono::Local::now().format("%Y-%m-%d %H:%M")
), ),
content, content,
description: Some("首次启动时自动导入".to_string()), description: Some("Automatically imported on first launch".to_string()),
enabled: true, // 自动启用 enabled: true, // 自动启用
created_at: Some(timestamp), created_at: Some(timestamp),
updated_at: Some(timestamp), updated_at: Some(timestamp),
@@ -306,6 +303,7 @@ mod tests {
use tempfile::TempDir; use tempfile::TempDir;
struct TempHome { struct TempHome {
#[allow(dead_code)] // 字段通过 Drop trait 管理临时目录生命周期
dir: TempDir, dir: TempDir,
original_home: Option<String>, original_home: Option<String>,
original_userprofile: Option<String>, original_userprofile: Option<String>,
@@ -444,7 +442,10 @@ mod tests {
.expect("gemini prompt exists"); .expect("gemini prompt exists");
assert!(prompt.enabled, "gemini prompt should be enabled"); assert!(prompt.enabled, "gemini prompt should be enabled");
assert_eq!(prompt.content, "# Gemini Prompt\n\nTest content"); assert_eq!(prompt.content, "# Gemini Prompt\n\nTest content");
assert_eq!(prompt.description, Some("首次启动时自动导入".to_string())); assert_eq!(
prompt.description,
Some("Automatically imported on first launch".to_string())
);
} }
#[test] #[test]
@@ -463,8 +464,35 @@ mod tests {
assert_eq!(config.prompts.gemini.prompts.len(), 1); assert_eq!(config.prompts.gemini.prompts.len(), 1);
// 验证所有提示词都被启用 // 验证所有提示词都被启用
assert!(config.prompts.claude.prompts.values().next().unwrap().enabled); assert!(
assert!(config.prompts.codex.prompts.values().next().unwrap().enabled); config
assert!(config.prompts.gemini.prompts.values().next().unwrap().enabled); .prompts
.claude
.prompts
.values()
.next()
.unwrap()
.enabled
);
assert!(
config
.prompts
.codex
.prompts
.values()
.next()
.unwrap()
.enabled
);
assert!(
config
.prompts
.gemini
.prompts
.values()
.next()
.unwrap()
.enabled
);
} }
} }

View File

@@ -58,9 +58,7 @@ pub async fn import_prompt_from_file(
} }
#[tauri::command] #[tauri::command]
pub async fn get_current_prompt_file_content( pub async fn get_current_prompt_file_content(app: String) -> Result<Option<String>, String> {
app: String,
) -> Result<Option<String>, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
PromptService::get_current_file_content(app_type).map_err(|e| e.to_string()) PromptService::get_current_file_content(app_type).map_err(|e| e.to_string())
} }

View File

@@ -8,23 +8,9 @@ use crate::gemini_config::get_gemini_dir;
/// 返回指定应用所使用的提示词文件路径。 /// 返回指定应用所使用的提示词文件路径。
pub fn prompt_file_path(app: &AppType) -> Result<PathBuf, AppError> { pub fn prompt_file_path(app: &AppType) -> Result<PathBuf, AppError> {
let base_dir = match app { let base_dir: PathBuf = match app {
AppType::Claude => get_claude_settings_path() AppType::Claude => get_base_dir_with_fallback(get_claude_settings_path(), ".claude")?,
.parent() AppType::Codex => get_base_dir_with_fallback(get_codex_auth_path(), ".codex")?,
.map(|p| p.to_path_buf())
.unwrap_or_else(|| {
dirs::home_dir()
.expect("无法获取用户目录")
.join(".claude")
}),
AppType::Codex => get_codex_auth_path()
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| {
dirs::home_dir()
.expect("无法获取用户目录")
.join(".codex")
}),
AppType::Gemini => get_gemini_dir(), AppType::Gemini => get_gemini_dir(),
}; };
@@ -36,3 +22,23 @@ pub fn prompt_file_path(app: &AppType) -> Result<PathBuf, AppError> {
Ok(base_dir.join(filename)) Ok(base_dir.join(filename))
} }
fn get_base_dir_with_fallback(
primary_path: PathBuf,
fallback_dir: &str,
) -> Result<PathBuf, AppError> {
primary_path
.parent()
.map(|p| p.to_path_buf())
.or_else(|| dirs::home_dir().map(|h| h.join(fallback_dir)))
.ok_or_else(|| {
AppError::localized(
"home_dir_not_found",
format!("无法确定 {} 配置目录:用户主目录不存在", fallback_dir),
format!(
"Cannot determine {} config directory: user home not found",
fallback_dir
),
)
})
}

View File

@@ -61,9 +61,7 @@ impl PromptService {
if let Some(prompt) = prompts.get(id) { if let Some(prompt) = prompts.get(id) {
if prompt.enabled { if prompt.enabled {
return Err(AppError::InvalidInput( return Err(AppError::InvalidInput("无法删除已启用的提示词".to_string()));
"无法删除已启用的提示词".to_string(),
));
} }
} }
@@ -92,7 +90,8 @@ impl PromptService {
if let Ok(content) = std::fs::read_to_string(&target_path) { if let Ok(content) = std::fs::read_to_string(&target_path) {
if !content.trim().is_empty() { if !content.trim().is_empty() {
// 检查是否已存在相同内容的提示词,避免重复备份 // 检查是否已存在相同内容的提示词,避免重复备份
let content_exists = prompts.values().any(|p| p.content.trim() == content.trim()); let content_exists =
prompts.values().any(|p| p.content.trim() == content.trim());
if !content_exists { if !content_exists {
let timestamp = std::time::SystemTime::now() let timestamp = std::time::SystemTime::now()
@@ -102,7 +101,10 @@ impl PromptService {
let backup_id = format!("backup-{timestamp}"); let backup_id = format!("backup-{timestamp}");
let backup_prompt = Prompt { let backup_prompt = Prompt {
id: backup_id.clone(), id: backup_id.clone(),
name: format!("原始提示词 {}", chrono::Local::now().format("%Y-%m-%d %H:%M")), name: format!(
"原始提示词 {}",
chrono::Local::now().format("%Y-%m-%d %H:%M")
),
content, content,
description: Some("自动备份的原始提示词".to_string()), description: Some("自动备份的原始提示词".to_string()),
enabled: false, enabled: false,
@@ -148,7 +150,8 @@ impl PromptService {
return Err(AppError::Message("提示词文件不存在".to_string())); return Err(AppError::Message("提示词文件不存在".to_string()));
} }
let content = std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?; let content =
std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?;
let timestamp = std::time::SystemTime::now() let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap() .unwrap()
@@ -177,7 +180,8 @@ impl PromptService {
if !file_path.exists() { if !file_path.exists() {
return Ok(None); return Ok(None);
} }
let content = std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?; let content =
std::fs::read_to_string(&file_path).map_err(|e| AppError::io(&file_path, e))?;
Ok(Some(content)) Ok(Some(content))
} }
} }