From e0a7f67dab00f9241d1cd09e747a896fe27a7307 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Sat, 9 May 2026 14:38:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9=20user=5Fop?= =?UTF-8?q?enclaw=20=E6=8A=80=E8=83=BD=E6=BA=90=E7=9A=84=E6=94=AF=E6=8C=81?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E5=92=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/mod.rs | 2 ++ src/skills/mod.rs | 62 +++++++++++++++++++++++++++++++++++++-- src/tools/skill_manage.rs | 4 ++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index fec0690..3d86d24 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -72,6 +72,7 @@ fn default_skills_sources() -> Vec { vec![ "user".to_string(), "user_agent".to_string(), + "user_openclaw".to_string(), "project".to_string(), "project_agent".to_string(), "project_openclaw".to_string(), @@ -893,6 +894,7 @@ mod tests { vec![ "user".to_string(), "user_agent".to_string(), + "user_openclaw".to_string(), "project".to_string(), "project_agent".to_string(), "project_openclaw".to_string(), diff --git a/src/skills/mod.rs b/src/skills/mod.rs index d833a90..d639eb6 100644 --- a/src/skills/mod.rs +++ b/src/skills/mod.rs @@ -28,6 +28,7 @@ pub struct Skill { pub enum SkillSource { User, UserAgent, + UserOpenclaw, Project, ProjectAgent, ProjectOpenclaw, @@ -328,6 +329,7 @@ impl SkillSource { match self { SkillSource::User => "user", SkillSource::UserAgent => "user_agent", + SkillSource::UserOpenclaw => "user_openclaw", SkillSource::Project => "project", SkillSource::ProjectAgent => "project_agent", SkillSource::ProjectOpenclaw => "project_openclaw", @@ -557,6 +559,11 @@ fn source_order(sources: &[String]) -> Vec { result.push(SkillSource::UserAgent); } } + "user_openclaw" => { + if !result.contains(&SkillSource::UserOpenclaw) { + result.push(SkillSource::UserOpenclaw); + } + } "project" => { if !result.contains(&SkillSource::Project) { result.push(SkillSource::Project); @@ -582,6 +589,7 @@ fn source_order(sources: &[String]) -> Vec { vec![ SkillSource::User, SkillSource::UserAgent, + SkillSource::UserOpenclaw, SkillSource::Project, SkillSource::ProjectAgent, SkillSource::ProjectOpenclaw, @@ -619,22 +627,34 @@ fn project_skill_state_path(cwd: &Path) -> PathBuf { cwd.join(".picobot").join("skill-state.json") } +fn home_dir() -> Option { + // First check HOME environment variable (useful for testing) + std::env::var_os("HOME") + .map(PathBuf::from) + .or_else(|| dirs::home_dir()) +} + fn user_skills_root() -> Option { - dirs::home_dir().map(|p| p.join(".picobot").join("skills")) + home_dir().map(|p| p.join(".picobot").join("skills")) } fn user_skill_state_path() -> Option { - dirs::home_dir().map(|p| p.join(".picobot").join("skill-state.json")) + home_dir().map(|p| p.join(".picobot").join("skill-state.json")) } fn user_agent_skills_root() -> Option { - dirs::home_dir().map(|p| p.join(".agents").join("skills")) + home_dir().map(|p| p.join(".agents").join("skills")) +} + +fn user_openclaw_skills_root() -> Option { + home_dir().map(|p| p.join(".openclaw").join("skills")) } fn source_root(source: SkillSource, cwd: &Path) -> Option { match source { SkillSource::User => user_skills_root(), SkillSource::UserAgent => user_agent_skills_root(), + SkillSource::UserOpenclaw => user_openclaw_skills_root(), SkillSource::Project => Some(cwd.join(".picobot").join("skills")), SkillSource::ProjectAgent => Some(project_agent_skills_root(cwd)), SkillSource::ProjectOpenclaw => Some(project_openclaw_skills_root(cwd)), @@ -1036,6 +1056,7 @@ mod tests { let ordered = source_order(&[ "user".to_string(), "user_agent".to_string(), + "user_openclaw".to_string(), "project".to_string(), "project_agent".to_string(), "project_openclaw".to_string(), @@ -1048,6 +1069,7 @@ mod tests { vec![ SkillSource::User, SkillSource::UserAgent, + SkillSource::UserOpenclaw, SkillSource::Project, SkillSource::ProjectAgent, SkillSource::ProjectOpenclaw, @@ -1293,4 +1315,38 @@ mod tests { let payload = catalog.activation_event_payload("demo-openclaw").unwrap(); assert_eq!(payload["source"], "project_openclaw"); } + + #[test] + fn test_discover_loads_user_openclaw_skills() { + let _lock = acquire_test_lock(); + let temp_dir = tempfile::tempdir().unwrap(); + let home_dir = temp_dir.path().join("home"); + let project_dir = temp_dir.path().join("project"); + fs::create_dir_all(&home_dir).unwrap(); + fs::create_dir_all(&project_dir).unwrap(); + let _home = HomeDirGuard::enter(&home_dir); + let _guard = CurrentDirGuard::enter(&project_dir); + + let user_openclaw_skill_dir = home_dir + .join(".openclaw") + .join("skills") + .join("demo-user-openclaw"); + fs::create_dir_all(&user_openclaw_skill_dir).unwrap(); + fs::write( + user_openclaw_skill_dir.join("SKILL.md"), + "---\ndescription: user openclaw skill\n---\nUse user openclaw", + ) + .unwrap(); + + let catalog = SkillCatalog::discover(&SkillsConfig { + enabled: true, + sources: vec!["user_openclaw".to_string()], + max_index_chars: 4000, + max_listed_skills: 32, + }); + + assert_eq!(catalog.len(), 1); + let payload = catalog.activation_event_payload("demo-user-openclaw").unwrap(); + assert_eq!(payload["source"], "user_openclaw"); + } } diff --git a/src/tools/skill_manage.rs b/src/tools/skill_manage.rs index a66102d..ff9b6cb 100644 --- a/src/tools/skill_manage.rs +++ b/src/tools/skill_manage.rs @@ -32,7 +32,7 @@ impl Tool for SkillManageTool { } fn description(&self) -> &str { - "Manage PicoBot skills stored under .picobot/skills or ~/.picobot/skills, while discovery also reads .agents/skills, ~/.agents/skills, and .openclaw/skills. Supports actions: list, get, create, update, delete, disable, reload." + "Manage PicoBot skills stored under .picobot/skills or ~/.picobot/skills, while discovery also reads .agents/skills, ~/.agents/skills, .openclaw/skills, and ~/.openclaw/skills. Supports actions: list, get, create, update, delete, disable, reload." } fn parameters_schema(&self) -> serde_json::Value { @@ -121,6 +121,7 @@ impl Tool for SkillManageTool { "source": match skill.source { crate::skills::SkillSource::User => "user", crate::skills::SkillSource::UserAgent => "user_agent", + crate::skills::SkillSource::UserOpenclaw => "user_openclaw", crate::skills::SkillSource::Project => "project", crate::skills::SkillSource::ProjectAgent => "project_agent", crate::skills::SkillSource::ProjectOpenclaw => "project_openclaw", @@ -324,6 +325,7 @@ fn list_skills_payload(skills: &Arc) -> serde_json::Value { "source": match skill.source { crate::skills::SkillSource::User => "user", crate::skills::SkillSource::UserAgent => "user_agent", + crate::skills::SkillSource::UserOpenclaw => "user_openclaw", crate::skills::SkillSource::Project => "project", crate::skills::SkillSource::ProjectAgent => "project_agent", crate::skills::SkillSource::ProjectOpenclaw => "project_openclaw",