mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 18:05:37 +08:00
feat(skill): implement recursive scanning for skill repositories (#309)
Add recursive directory scanning to discover SKILL.md files in nested directories. When a SKILL.md is found, treat sibling directories as functional folders rather than separate skills.
This commit is contained in:
@@ -100,12 +100,13 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
write_json_file(&path, &provider.settings_config)?;
|
||||
}
|
||||
AppType::Codex => {
|
||||
let obj = provider.settings_config.as_object().ok_or_else(|| {
|
||||
AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string())
|
||||
})?;
|
||||
let auth = obj.get("auth").ok_or_else(|| {
|
||||
AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string())
|
||||
})?;
|
||||
let obj = provider
|
||||
.settings_config
|
||||
.as_object()
|
||||
.ok_or_else(|| AppError::Config("Codex 供应商配置必须是 JSON 对象".to_string()))?;
|
||||
let auth = obj
|
||||
.get("auth")
|
||||
.ok_or_else(|| AppError::Config("Codex 供应商配置缺少 'auth' 字段".to_string()))?;
|
||||
let config_str = obj.get("config").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||
AppError::Config("Codex 供应商配置缺少 'config' 字段或不是字符串".to_string())
|
||||
})?;
|
||||
@@ -113,8 +114,7 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
let auth_path = get_codex_auth_path();
|
||||
write_json_file(&auth_path, auth)?;
|
||||
let config_path = get_codex_config_path();
|
||||
std::fs::write(&config_path, config_str)
|
||||
.map_err(|e| AppError::io(&config_path, e))?;
|
||||
std::fs::write(&config_path, config_str).map_err(|e| AppError::io(&config_path, e))?;
|
||||
}
|
||||
AppType::Gemini => {
|
||||
// Delegate to write_gemini_live which handles env file writing correctly
|
||||
@@ -132,11 +132,11 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
||||
for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||
// Use validated effective current provider
|
||||
let current_id = match crate::settings::get_effective_current_provider(&state.db, &app_type)?
|
||||
{
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
let current_id =
|
||||
match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let providers = state.db.get_all_providers(app_type.as_str())?;
|
||||
if let Some(provider) = providers.get(¤t_id) {
|
||||
|
||||
@@ -168,9 +168,7 @@ pub(crate) fn validate_usage_script(script: &UsageScript) -> Result<(), AppError
|
||||
if interval > 1440 {
|
||||
return Err(AppError::localized(
|
||||
"usage_script.interval_too_large",
|
||||
format!(
|
||||
"自动查询间隔不能超过 1440 分钟(24小时),当前值: {interval}"
|
||||
),
|
||||
format!("自动查询间隔不能超过 1440 分钟(24小时),当前值: {interval}"),
|
||||
format!(
|
||||
"Auto query interval cannot exceed 1440 minutes (24 hours), current: {interval}"
|
||||
),
|
||||
|
||||
+157
-82
@@ -214,56 +214,8 @@ impl SkillService {
|
||||
temp_dir.clone()
|
||||
};
|
||||
|
||||
// 遍历目标目录
|
||||
for entry in fs::read_dir(&scan_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let skill_md = path.join("SKILL.md");
|
||||
if !skill_md.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析技能元数据
|
||||
match self.parse_skill_metadata(&skill_md) {
|
||||
Ok(meta) => {
|
||||
// 安全地获取目录名
|
||||
let Some(dir_name) = path.file_name() else {
|
||||
log::warn!("Failed to get directory name from path: {path:?}");
|
||||
continue;
|
||||
};
|
||||
let directory = dir_name.to_string_lossy().to_string();
|
||||
|
||||
// 构建 README URL(考虑 skillsPath)
|
||||
let readme_path = if let Some(ref skills_path) = repo.skills_path {
|
||||
format!("{}/{}", skills_path.trim_matches('/'), directory)
|
||||
} else {
|
||||
directory.clone()
|
||||
};
|
||||
|
||||
skills.push(Skill {
|
||||
key: format!("{}/{}:{}", repo.owner, repo.name, directory),
|
||||
name: meta.name.unwrap_or_else(|| directory.clone()),
|
||||
description: meta.description.unwrap_or_default(),
|
||||
directory,
|
||||
readme_url: Some(format!(
|
||||
"https://github.com/{}/{}/tree/{}/{}",
|
||||
repo.owner, repo.name, repo.branch, readme_path
|
||||
)),
|
||||
installed: false,
|
||||
repo_owner: Some(repo.owner.clone()),
|
||||
repo_name: Some(repo.name.clone()),
|
||||
repo_branch: Some(repo.branch.clone()),
|
||||
skills_path: repo.skills_path.clone(),
|
||||
});
|
||||
}
|
||||
Err(e) => log::warn!("解析 {} 元数据失败: {}", skill_md.display(), e),
|
||||
}
|
||||
}
|
||||
// 递归扫描目录查找所有技能
|
||||
self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?;
|
||||
|
||||
// 清理临时目录
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
@@ -271,6 +223,90 @@ impl SkillService {
|
||||
Ok(skills)
|
||||
}
|
||||
|
||||
/// 递归扫描目录查找 SKILL.md
|
||||
///
|
||||
/// 规则:
|
||||
/// 1. 如果当前目录存在 SKILL.md,则识别为技能,停止扫描其子目录(子目录视为功能文件夹)
|
||||
/// 2. 如果当前目录不存在 SKILL.md,则递归扫描所有子目录
|
||||
fn scan_dir_recursive(
|
||||
&self,
|
||||
current_dir: &Path,
|
||||
base_dir: &Path,
|
||||
repo: &SkillRepo,
|
||||
skills: &mut Vec<Skill>,
|
||||
) -> Result<()> {
|
||||
// 检查当前目录是否包含 SKILL.md
|
||||
let skill_md = current_dir.join("SKILL.md");
|
||||
|
||||
if skill_md.exists() {
|
||||
// 发现技能!获取相对路径作为目录名
|
||||
let directory = if current_dir == base_dir {
|
||||
// 根目录的 SKILL.md,使用仓库名
|
||||
repo.name.clone()
|
||||
} else {
|
||||
// 子目录的 SKILL.md,使用相对路径
|
||||
current_dir
|
||||
.strip_prefix(base_dir)
|
||||
.unwrap_or(current_dir)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
if let Ok(skill) = self.build_skill_from_metadata(&skill_md, &directory, repo) {
|
||||
skills.push(skill);
|
||||
}
|
||||
|
||||
// 停止扫描此目录的子目录(同级目录都是功能文件夹)
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 未发现 SKILL.md,继续递归扫描所有子目录
|
||||
for entry in fs::read_dir(current_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
// 只处理目录
|
||||
if path.is_dir() {
|
||||
self.scan_dir_recursive(&path, base_dir, repo, skills)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从 SKILL.md 构建技能对象
|
||||
fn build_skill_from_metadata(
|
||||
&self,
|
||||
skill_md: &Path,
|
||||
directory: &str,
|
||||
repo: &SkillRepo,
|
||||
) -> Result<Skill> {
|
||||
let meta = self.parse_skill_metadata(skill_md)?;
|
||||
|
||||
// 构建 README URL
|
||||
let readme_path = if let Some(ref skills_path) = repo.skills_path {
|
||||
format!("{}/{}", skills_path.trim_matches('/'), directory)
|
||||
} else {
|
||||
directory.to_string()
|
||||
};
|
||||
|
||||
Ok(Skill {
|
||||
key: format!("{}/{}:{}", repo.owner, repo.name, directory),
|
||||
name: meta.name.unwrap_or_else(|| directory.to_string()),
|
||||
description: meta.description.unwrap_or_default(),
|
||||
directory: directory.to_string(),
|
||||
readme_url: Some(format!(
|
||||
"https://github.com/{}/{}/tree/{}/{}",
|
||||
repo.owner, repo.name, repo.branch, readme_path
|
||||
)),
|
||||
installed: false,
|
||||
repo_owner: Some(repo.owner.clone()),
|
||||
repo_name: Some(repo.name.clone()),
|
||||
repo_branch: Some(repo.branch.clone()),
|
||||
skills_path: repo.skills_path.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 解析技能元数据
|
||||
fn parse_skill_metadata(&self, path: &Path) -> Result<SkillMetadata> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
@@ -302,25 +338,18 @@ impl SkillService {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(&self.install_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
// 收集所有本地技能
|
||||
let mut local_skills = Vec::new();
|
||||
self.scan_local_dir_recursive(&self.install_dir, &self.install_dir, &mut local_skills)?;
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
// 处理找到的本地技能
|
||||
for local_skill in local_skills {
|
||||
let directory = &local_skill.directory;
|
||||
|
||||
// 安全地获取目录名
|
||||
let Some(dir_name) = path.file_name() else {
|
||||
log::warn!("Failed to get directory name from path: {path:?}");
|
||||
continue;
|
||||
};
|
||||
let directory = dir_name.to_string_lossy().to_string();
|
||||
|
||||
// 更新已安装状态
|
||||
// 更新已安装状态(匹配远程技能)
|
||||
let mut found = false;
|
||||
for skill in skills.iter_mut() {
|
||||
if skill.directory.eq_ignore_ascii_case(&directory) {
|
||||
if skill.directory.eq_ignore_ascii_case(directory) {
|
||||
skill.installed = true;
|
||||
found = true;
|
||||
break;
|
||||
@@ -329,23 +358,69 @@ impl SkillService {
|
||||
|
||||
// 添加本地独有的技能(仅当在仓库中未找到时)
|
||||
if !found {
|
||||
let skill_md = path.join("SKILL.md");
|
||||
if skill_md.exists() {
|
||||
if let Ok(meta) = self.parse_skill_metadata(&skill_md) {
|
||||
skills.push(Skill {
|
||||
key: format!("local:{directory}"),
|
||||
name: meta.name.unwrap_or_else(|| directory.clone()),
|
||||
description: meta.description.unwrap_or_default(),
|
||||
directory: directory.clone(),
|
||||
readme_url: None,
|
||||
installed: true,
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
repo_branch: None,
|
||||
skills_path: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
skills.push(local_skill);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 递归扫描本地目录查找 SKILL.md
|
||||
fn scan_local_dir_recursive(
|
||||
&self,
|
||||
current_dir: &Path,
|
||||
base_dir: &Path,
|
||||
skills: &mut Vec<Skill>,
|
||||
) -> Result<()> {
|
||||
// 检查当前目录是否包含 SKILL.md
|
||||
let skill_md = current_dir.join("SKILL.md");
|
||||
|
||||
if skill_md.exists() {
|
||||
// 发现技能!获取相对路径作为目录名
|
||||
let directory = if current_dir == base_dir {
|
||||
// 如果是 install_dir 本身,使用最后一段路径名
|
||||
current_dir
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
} else {
|
||||
// 使用相对于 install_dir 的路径
|
||||
current_dir
|
||||
.strip_prefix(base_dir)
|
||||
.unwrap_or(current_dir)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
// 解析元数据并创建本地技能对象
|
||||
if let Ok(meta) = self.parse_skill_metadata(&skill_md) {
|
||||
skills.push(Skill {
|
||||
key: format!("local:{directory}"),
|
||||
name: meta.name.unwrap_or_else(|| directory.clone()),
|
||||
description: meta.description.unwrap_or_default(),
|
||||
directory: directory.clone(),
|
||||
readme_url: None,
|
||||
installed: true,
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
repo_branch: None,
|
||||
skills_path: None,
|
||||
});
|
||||
}
|
||||
|
||||
// 停止扫描此目录的子目录(同级目录都是功能文件夹)
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 未发现 SKILL.md,继续递归扫描所有子目录
|
||||
for entry in fs::read_dir(current_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
// 只处理目录
|
||||
if path.is_dir() {
|
||||
self.scan_local_dir_recursive(&path, base_dir, skills)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user