feat(skills): enhance error messages with i18n support
- Add structured error format with error codes and context - Create skillErrorParser to format errors for user-friendly display - Add comprehensive i18n keys for all skill-related errors (zh/en) - Extend download timeout from 15s to 60s to reduce false positives - Fix: Pass correct error title based on operation context (load/install/uninstall) Error improvements: - SKILL_NOT_FOUND: Show skill directory name - DOWNLOAD_TIMEOUT: Display repo info and timeout duration with network suggestion - DOWNLOAD_FAILED: Show HTTP status code with specific suggestions (403/404/429) - SKILL_DIR_NOT_FOUND: Show full path with URL check suggestion - EMPTY_ARCHIVE: Suggest checking repository URL Users will now see detailed error messages instead of generic "Error".
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::error::format_skill_error;
|
||||
use crate::services::skill::SkillState;
|
||||
use crate::services::{Skill, SkillRepo, SkillService};
|
||||
use crate::store::AppState;
|
||||
@@ -45,18 +46,36 @@ pub async fn install_skill(
|
||||
let skill = skills
|
||||
.iter()
|
||||
.find(|s| s.directory.eq_ignore_ascii_case(&directory))
|
||||
.ok_or_else(|| "技能不存在".to_string())?;
|
||||
.ok_or_else(|| {
|
||||
format_skill_error(
|
||||
"SKILL_NOT_FOUND",
|
||||
&[("directory", &directory)],
|
||||
Some("checkRepoUrl"),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !skill.installed {
|
||||
let repo = SkillRepo {
|
||||
owner: skill
|
||||
.repo_owner
|
||||
.clone()
|
||||
.ok_or_else(|| "缺少仓库信息".to_string())?,
|
||||
.ok_or_else(|| {
|
||||
format_skill_error(
|
||||
"MISSING_REPO_INFO",
|
||||
&[("directory", &directory), ("field", "owner")],
|
||||
None,
|
||||
)
|
||||
})?,
|
||||
name: skill
|
||||
.repo_name
|
||||
.clone()
|
||||
.ok_or_else(|| "缺少仓库信息".to_string())?,
|
||||
.ok_or_else(|| {
|
||||
format_skill_error(
|
||||
"MISSING_REPO_INFO",
|
||||
&[("directory", &directory), ("field", "name")],
|
||||
None,
|
||||
)
|
||||
})?,
|
||||
branch: skill
|
||||
.repo_branch
|
||||
.clone()
|
||||
|
||||
@@ -94,3 +94,28 @@ impl From<AppError> for String {
|
||||
err.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// 格式化为 JSON 错误字符串,前端可解析为结构化错误
|
||||
pub fn format_skill_error(
|
||||
code: &str,
|
||||
context: &[(&str, &str)],
|
||||
suggestion: Option<&str>,
|
||||
) -> String {
|
||||
use serde_json::json;
|
||||
|
||||
let mut ctx_map = serde_json::Map::new();
|
||||
for (key, value) in context {
|
||||
ctx_map.insert(key.to_string(), json!(value));
|
||||
}
|
||||
|
||||
let error_obj = json!({
|
||||
"code": code,
|
||||
"context": ctx_map,
|
||||
"suggestion": suggestion,
|
||||
});
|
||||
|
||||
serde_json::to_string(&error_obj).unwrap_or_else(|_| {
|
||||
// 如果 JSON 序列化失败,返回简单格式
|
||||
format!("ERROR:{}", code)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::error::format_skill_error;
|
||||
|
||||
/// 技能对象
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Skill {
|
||||
@@ -133,7 +135,11 @@ impl SkillService {
|
||||
}
|
||||
|
||||
fn get_install_dir() -> Result<PathBuf> {
|
||||
let home = dirs::home_dir().context("无法获取用户主目录")?;
|
||||
let home = dirs::home_dir().context(format_skill_error(
|
||||
"GET_HOME_DIR_FAILED",
|
||||
&[],
|
||||
Some("checkPermission"),
|
||||
))?;
|
||||
Ok(home.join(".claude").join("skills"))
|
||||
}
|
||||
}
|
||||
@@ -173,9 +179,19 @@ impl SkillService {
|
||||
/// 从仓库获取技能列表
|
||||
async fn fetch_repo_skills(&self, repo: &SkillRepo) -> Result<Vec<Skill>> {
|
||||
// 为单个仓库加载增加整体超时,避免无效链接长时间阻塞
|
||||
let temp_dir = timeout(std::time::Duration::from_secs(15), self.download_repo(repo))
|
||||
let temp_dir = timeout(std::time::Duration::from_secs(60), self.download_repo(repo))
|
||||
.await
|
||||
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
|
||||
.map_err(|_| {
|
||||
anyhow!(format_skill_error(
|
||||
"DOWNLOAD_TIMEOUT",
|
||||
&[
|
||||
("owner", &repo.owner),
|
||||
("name", &repo.name),
|
||||
("timeout", "60")
|
||||
],
|
||||
Some("checkNetwork"),
|
||||
))
|
||||
})??;
|
||||
let mut skills = Vec::new();
|
||||
|
||||
// 确定要扫描的目录路径
|
||||
@@ -379,7 +395,17 @@ impl SkillService {
|
||||
// 下载 ZIP
|
||||
let response = self.http_client.get(url).send().await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!("下载失败: {}", response.status()));
|
||||
let status = response.status().as_u16().to_string();
|
||||
return Err(anyhow::anyhow!(format_skill_error(
|
||||
"DOWNLOAD_FAILED",
|
||||
&[("status", &status)],
|
||||
match status.as_str() {
|
||||
"403" => Some("http403"),
|
||||
"404" => Some("http404"),
|
||||
"429" => Some("http429"),
|
||||
_ => Some("checkNetwork"),
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
@@ -394,7 +420,11 @@ impl SkillService {
|
||||
let name = first_file.name();
|
||||
name.split('/').next().unwrap_or("").to_string()
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("空的压缩包"));
|
||||
return Err(anyhow::anyhow!(format_skill_error(
|
||||
"EMPTY_ARCHIVE",
|
||||
&[],
|
||||
Some("checkRepoUrl"),
|
||||
)));
|
||||
};
|
||||
|
||||
// 解压所有文件
|
||||
@@ -441,11 +471,21 @@ impl SkillService {
|
||||
|
||||
// 下载仓库时增加总超时,防止无效链接导致长时间卡住安装过程
|
||||
let temp_dir = timeout(
|
||||
std::time::Duration::from_secs(15),
|
||||
std::time::Duration::from_secs(60),
|
||||
self.download_repo(&repo),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| anyhow!("下载仓库 {}/{} 超时", repo.owner, repo.name))??;
|
||||
.map_err(|_| {
|
||||
anyhow!(format_skill_error(
|
||||
"DOWNLOAD_TIMEOUT",
|
||||
&[
|
||||
("owner", &repo.owner),
|
||||
("name", &repo.name),
|
||||
("timeout", "60")
|
||||
],
|
||||
Some("checkNetwork"),
|
||||
))
|
||||
})??;
|
||||
|
||||
// 根据 skills_path 确定源目录路径
|
||||
let source = if let Some(ref skills_path) = repo.skills_path {
|
||||
@@ -458,10 +498,11 @@ impl SkillService {
|
||||
|
||||
if !source.exists() {
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
return Err(anyhow::anyhow!(
|
||||
"技能目录不存在: {}",
|
||||
source.display()
|
||||
));
|
||||
return Err(anyhow::anyhow!(format_skill_error(
|
||||
"SKILL_DIR_NOT_FOUND",
|
||||
&[("path", &source.display().to_string())],
|
||||
Some("checkRepoUrl"),
|
||||
)));
|
||||
}
|
||||
|
||||
// 删除旧版本
|
||||
|
||||
Reference in New Issue
Block a user