feat: route staged scene skills through runtime

Add registry-driven scene routing and multi-root skill loading so fault-details and 95598 scene skills can be triggered from natural language while still running through the browser-backed runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-07 16:17:17 +08:00
parent bdf8e12246
commit 96c3bf1dee
21 changed files with 2846 additions and 240 deletions

View File

@@ -4,8 +4,8 @@ use std::sync::{Mutex, OnceLock};
use sgclaw::compat::config_adapter::{
build_zeroclaw_config, build_zeroclaw_config_from_settings,
build_zeroclaw_config_from_sgclaw_settings, resolve_skills_dir, zeroclaw_default_skills_dir,
zeroclaw_workspace_dir,
build_zeroclaw_config_from_sgclaw_settings, resolve_scene_skills_dir_path, resolve_skills_dir,
zeroclaw_default_skills_dir, zeroclaw_workspace_dir,
};
use sgclaw::config::{
BrowserBackend, DeepSeekSettings, OfficeBackend, PlannerMode, SgClawSettings, SkillsPromptMode,
@@ -47,7 +47,7 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
api_key: "key".to_string(),
base_url: "https://proxy.example.com/v1".to_string(),
model: "deepseek-reasoner".to_string(),
skills_dir: None,
skills_dir: Vec::new(),
};
let workspace_dir = zeroclaw_workspace_dir(Path::new("/var/lib/sgclaw"));
@@ -66,7 +66,7 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
);
assert_eq!(
resolve_skills_dir(Path::new("/var/lib/sgclaw"), &settings),
zeroclaw_default_skills_dir(Path::new("/var/lib/sgclaw"))
vec![zeroclaw_default_skills_dir(Path::new("/var/lib/sgclaw"))]
);
}
@@ -92,7 +92,7 @@ fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
assert_eq!(first.api_key, "sk-first");
assert_eq!(first.base_url, "https://api.deepseek.com");
assert_eq!(first.model, "deepseek-chat");
assert_eq!(first.skills_dir, None);
assert!(first.skills_dir.is_empty());
fs::write(
&config_path,
@@ -111,7 +111,7 @@ fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
assert_eq!(second.api_key, "sk-second");
assert_eq!(second.base_url, "https://proxy.example.com/v1");
assert_eq!(second.model, "deepseek-reasoner");
assert_eq!(second.skills_dir, Some(root.join("skill_lib")));
assert_eq!(second.skills_dir, vec![root.join("skill_lib")]);
}
#[test]
@@ -122,12 +122,12 @@ fn resolve_skills_dir_prefers_nested_skills_subdirectory_for_configured_repo_roo
api_key: "key".to_string(),
base_url: "https://api.deepseek.com".to_string(),
model: "deepseek-chat".to_string(),
skills_dir: Some(root.join("skill_lib")),
skills_dir: vec![root.join("skill_lib")],
};
let resolved = resolve_skills_dir(&root, &settings);
assert_eq!(resolved, root.join("skill_lib/skills"));
assert_eq!(resolved, vec![root.join("skill_lib/skills")]);
}
#[test]
@@ -139,12 +139,41 @@ fn resolve_skills_dir_preserves_absolute_configured_skills_directory() {
api_key: "key".to_string(),
base_url: "https://api.deepseek.com".to_string(),
model: "deepseek-chat".to_string(),
skills_dir: Some(external_skills.clone()),
skills_dir: vec![external_skills.clone()],
};
let resolved = resolve_skills_dir(&root, &settings);
assert_eq!(resolved, external_skills);
assert_eq!(resolved, vec![external_skills]);
}
#[test]
fn resolve_skills_dir_uses_skills_child_for_external_staged_root() {
let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4()));
let staged_root = root.join("external/skill_staging");
fs::create_dir_all(staged_root.join("skills")).unwrap();
fs::create_dir_all(staged_root.join("scenes")).unwrap();
let settings = DeepSeekSettings {
api_key: "key".to_string(),
base_url: "https://api.deepseek.com".to_string(),
model: "deepseek-chat".to_string(),
skills_dir: vec![staged_root.clone()],
};
let resolved = resolve_skills_dir(&root, &settings);
assert_eq!(resolved, vec![staged_root.join("skills")]);
}
#[test]
fn resolve_scene_skills_dir_path_prefers_staged_skills_child_under_project_root() {
let root = std::env::temp_dir().join(format!("sgclaw-scene-skills-{}", Uuid::new_v4()));
let top_level_skills = root.join("project/skills");
fs::create_dir_all(top_level_skills.join("skill_staging/skills")).unwrap();
let resolved = resolve_scene_skills_dir_path(top_level_skills.clone());
assert_eq!(resolved, top_level_skills.join("skill_staging/skills"));
}
#[test]
@@ -153,7 +182,7 @@ fn sgclaw_settings_default_to_compact_skills_and_browser_attached_profile() {
"sk-test".to_string(),
"https://api.deepseek.com".to_string(),
"deepseek-chat".to_string(),
None,
Vec::new(),
)
.unwrap();
@@ -187,7 +216,7 @@ fn sgclaw_settings_load_new_runtime_fields_from_browser_config() {
assert_eq!(settings.runtime_profile, RuntimeProfile::GeneralAssistant);
assert_eq!(settings.skills_prompt_mode, SkillsPromptMode::Full);
assert_eq!(settings.skills_dir, Some(root.join("skill_lib")));
assert_eq!(settings.skills_dir, vec![root.join("skill_lib")]);
assert_eq!(config.skills.prompt_injection_mode, SkillsPromptMode::Full);
}
@@ -251,7 +280,7 @@ fn browser_attached_config_uses_low_temperature_for_deterministic_execution() {
"sk-test".to_string(),
"https://api.deepseek.com".to_string(),
"deepseek-chat".to_string(),
None,
Vec::new(),
)
.unwrap();