feat: 导入的 skills 默认存到 ~/.ias/skills/

- Import/Delete 目标改为 ~/.ias/skills/(全局共享)
- SkillRegistry::load() 返回 (Self, Vec<String>) 替代 Result
  允许部分加载:格式不兼容的 skill 记录警告但不阻止加载
- 新增 global_skills_dir() 辅助函数
This commit is contained in:
2026-06-02 10:02:54 +08:00
parent e88f3240c4
commit f7ba23db94
13 changed files with 40 additions and 1570 deletions
+22 -17
View File
@@ -206,15 +206,13 @@ async fn cmd_listen(
}
// 加载系统 Skills
let skill_registry = match SkillRegistry::load("resources/skills", "skills").await {
Ok(r) => {
info!("已加载 {} 个技能", r.count());
Some(Arc::new(r))
}
Err(e) => {
info!("技能加载结果: {:?}", e);
None
let (skill_registry, _warnings) = SkillRegistry::load("resources/skills", "skills").await;
let skill_registry = {
if !_warnings.is_empty() {
info!("技能加载警告: {:?}", _warnings);
}
info!("已加载 {} 个技能", skill_registry.count());
Some(Arc::new(skill_registry))
};
// 审批管理器
@@ -795,10 +793,19 @@ async fn cmd_send(
}
}
fn global_skills_dir() -> std::path::PathBuf {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".ias")
.join("skills")
}
async fn cmd_skills(action: SkillsAction) {
use dialoguer::{MultiSelect, theme::ColorfulTheme};
use console::Term;
let global_dir = global_skills_dir();
match action {
SkillsAction::List => {
let discovered = skills_importer::discover_skills();
@@ -833,8 +840,8 @@ async fn cmd_skills(action: SkillsAction) {
return;
}
let dest = std::path::PathBuf::from("skills");
std::fs::create_dir_all(&dest).ok();
let dest = &global_dir;
std::fs::create_dir_all(dest).ok();
let mut imported = 0;
let mut skipped = 0;
@@ -852,18 +859,16 @@ async fn cmd_skills(action: SkillsAction) {
}
}
}
println!("\n完成:导入 {imported} 个,跳过 {skipped}");
println!("重启 iAs 后生效");
println!("\n完成:导入 {imported} 个,跳过 {skipped} (→ {})", global_dir.display());
}
SkillsAction::Delete => {
let skills_dir = std::path::PathBuf::from("skills");
if !skills_dir.exists() {
println!("skills 目录不存在,没有可删除的 Skill");
if !global_dir.exists() {
println!("全局 skills 目录不存在,没有可删除的 Skill");
return;
}
let mut entries: Vec<(String, String)> = Vec::new();
if let Ok(dir) = std::fs::read_dir(&skills_dir) {
if let Ok(dir) = std::fs::read_dir(&global_dir) {
for entry in dir.flatten() {
let path = entry.path();
if path.is_dir() && path.join("SKILL.md").exists() {
@@ -901,7 +906,7 @@ async fn cmd_skills(action: SkillsAction) {
let mut deleted = 0;
for idx in selection {
let (name, _) = &entries[idx];
let path = skills_dir.join(name);
let path = global_dir.join(name);
match std::fs::remove_dir_all(&path) {
Ok(_) => {
println!(" ✅ 已删除 {name}");
+16 -21
View File
@@ -210,32 +210,27 @@ mod tests {
#[test]
fn test_load_all_system_skills() {
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(crate::tools::registry::SkillRegistry::load(
let (registry, errors) = rt.block_on(crate::tools::registry::SkillRegistry::load(
"resources/skills",
"/nonexistent",
));
match result {
Ok(registry) => {
assert_eq!(registry.count(), 9, "应该有 9 个系统技能");
let names: Vec<&str> = registry.list_specs().iter().map(|s| s.name.as_str()).collect();
println!("已加载技能: {:?}", names);
assert!(names.contains(&"manage_memos"));
assert!(names.contains(&"manage_skill"));
assert!(names.contains(&"get_current_datetime"));
assert!(names.contains(&"query_weather"));
assert!(names.contains(&"search_web"));
assert!(names.contains(&"email"));
assert!(names.contains(&"execute_shell"));
assert!(names.contains(&"list_scheduled_tasks"));
assert!(names.contains(&"manage_scheduled_task"));
}
Err(errors) => {
for e in &errors {
eprintln!("加载失败: {}", e);
}
panic!("技能加载失败");
if !errors.is_empty() {
for e in &errors {
eprintln!("加载警告: {}", e);
}
}
assert_eq!(registry.count(), 9, "应该有 9 个系统技能");
let names: Vec<&str> = registry.list_specs().iter().map(|s| s.name.as_str()).collect();
println!("已加载技能: {:?}", names);
assert!(names.contains(&"manage_memos"));
assert!(names.contains(&"manage_skill"));
assert!(names.contains(&"get_current_datetime"));
assert!(names.contains(&"query_weather"));
assert!(names.contains(&"search_web"));
assert!(names.contains(&"email"));
assert!(names.contains(&"execute_shell"));
assert!(names.contains(&"list_scheduled_tasks"));
assert!(names.contains(&"manage_scheduled_task"));
}
#[test]
+2 -9
View File
@@ -22,10 +22,7 @@ impl SkillRegistry {
}
/// 从 system_dir 和 user_dir 加载所有技能
/// system_dir: resources/skills/(随 iAs 分发)
/// user_dir: skills/(用户自行添加)
/// 同名时 user 覆盖 system
pub async fn load(system_dir: &str, user_dir: &str) -> Result<Self, Vec<String>> {
pub async fn load(system_dir: &str, user_dir: &str) -> (Self, Vec<String>) {
let mut registry = Self::new();
let mut errors = Vec::new();
@@ -47,11 +44,7 @@ impl SkillRegistry {
registry.load_from_dir(user_dir, SkillSource::User, &mut errors);
}
if errors.is_empty() {
Ok(registry)
} else {
Err(errors)
}
(registry, errors)
}
fn load_from_dir(&mut self, dir: &str, source: SkillSource, errors: &mut Vec<String>) {