use std::fs; use std::path::{Path, PathBuf}; use serde::Deserialize; use serde_json::{Map, Value}; const STAGED_SCENE_ROOT: &str = "D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DispatchMode { DirectBrowser, AgentBrowser, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SceneRegistryEntry { pub id: String, pub name: String, pub summary: String, pub tags: Vec, pub inputs: Vec, pub outputs: Vec, pub skill_package: String, pub skill_tool: String, pub skill_artifact_type: String, pub dispatch_mode: DispatchMode, pub expected_domain: String, pub aliases: Vec, pub default_args: Map, } #[derive(Debug, Deserialize)] struct SceneMetadata { id: String, name: String, summary: String, #[serde(default)] tags: Vec, #[serde(default)] inputs: Vec, #[serde(default)] outputs: Vec, skill: SceneSkillMetadata, } #[derive(Debug, Deserialize)] struct SceneSkillMetadata { package: String, tool: String, artifact_type: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct SceneMatchScore { matched_terms: usize, longest_term: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct SceneMatchResult { score: SceneMatchScore, has_strong_phrase_hit: bool, } #[derive(Debug, Clone)] struct SceneRuntimePolicy { scene_id: &'static str, dispatch_mode: DispatchMode, expected_domain: &'static str, aliases: &'static [&'static str], } const FIRST_SLICE_POLICIES: [SceneRuntimePolicy; 2] = [ SceneRuntimePolicy { scene_id: "fault-details-report", dispatch_mode: DispatchMode::DirectBrowser, expected_domain: "sgcc.example.invalid", aliases: &["故障明细", "故障明细报表", "导出故障明细"], }, SceneRuntimePolicy { scene_id: "95598-repair-city-dispatch", dispatch_mode: DispatchMode::AgentBrowser, expected_domain: "95598.example.invalid", aliases: &["95598抢修市指", "市指抢修监测", "95598抢修队列", "95598抢修市指监测"], }, ]; pub fn load_first_slice_scene_registry() -> Vec { load_scene_registry_from_root(Path::new(STAGED_SCENE_ROOT)) } pub fn load_scene_registry_from_root(root: &Path) -> Vec { let mut registry = Vec::new(); for policy in FIRST_SLICE_POLICIES { if let Some(entry) = load_scene_entry(root, &policy) { registry.push(entry); } } registry } pub fn match_scene_instruction(instruction: &str) -> Option { let registry = load_first_slice_scene_registry(); match_scene_instruction_in_registry(®istry, instruction) } pub fn match_scene_instruction_in_registry( registry: &[SceneRegistryEntry], instruction: &str, ) -> Option { let normalized_instruction = normalize_for_match(instruction); if normalized_instruction.is_empty() { return None; } let mut best_match: Option<(SceneMatchResult, &SceneRegistryEntry)> = None; let mut ambiguous = false; let mut strong_phrase_hits = 0; for entry in registry { let Some(result) = score_scene_instruction(entry, &normalized_instruction) else { continue; }; if result.has_strong_phrase_hit { strong_phrase_hits += 1; } match &best_match { None => { best_match = Some((result, entry)); ambiguous = false; } Some((current_result, _)) if result.score > current_result.score => { best_match = Some((result, entry)); ambiguous = false; } Some((current_result, current_entry)) if result.score == current_result.score => { if result.has_strong_phrase_hit && current_result.has_strong_phrase_hit { if current_entry.id != entry.id { ambiguous = true; } } else if result.has_strong_phrase_hit && !current_result.has_strong_phrase_hit { best_match = Some((result, entry)); ambiguous = false; } else if current_result.has_strong_phrase_hit && !result.has_strong_phrase_hit { ambiguous = false; } else if current_entry.id != entry.id { ambiguous = true; } } Some(_) => {} } } if ambiguous || strong_phrase_hits > 1 { None } else { best_match.map(|(_, entry)| entry.clone()) } } fn load_scene_entry(root: &Path, policy: &SceneRuntimePolicy) -> Option { let scene_path = scene_json_path(root, policy.scene_id); let contents = fs::read_to_string(scene_path).ok()?; let metadata: SceneMetadata = serde_json::from_str(&contents).ok()?; if metadata.id != policy.scene_id { return None; } Some(SceneRegistryEntry { id: policy.scene_id.to_string(), name: metadata.name, summary: metadata.summary, tags: metadata.tags, inputs: metadata.inputs, outputs: metadata.outputs, skill_package: metadata.skill.package, skill_tool: metadata.skill.tool, skill_artifact_type: metadata.skill.artifact_type, dispatch_mode: policy.dispatch_mode, expected_domain: policy.expected_domain.to_string(), aliases: policy.aliases.iter().map(|alias| (*alias).to_string()).collect(), default_args: Map::new(), }) } fn scene_json_path(root: &Path, scene_id: &str) -> PathBuf { root.join("scenes").join(scene_id).join("scene.json") } fn score_scene_instruction( entry: &SceneRegistryEntry, normalized_instruction: &str, ) -> Option { let mut matched_terms = 0; let mut longest_term = 0; let mut has_strong_phrase_hit = false; for term in candidate_match_terms(entry) { let normalized_term = normalize_for_match(&term); if normalized_term.len() < 2 { continue; } if normalized_instruction.contains(&normalized_term) { matched_terms += 1; longest_term = longest_term.max(normalized_term.len()); if normalized_term.len() >= 6 { has_strong_phrase_hit = true; } } } if matched_terms == 0 { None } else { Some(SceneMatchResult { score: SceneMatchScore { matched_terms, longest_term, }, has_strong_phrase_hit, }) } } fn candidate_match_terms(entry: &SceneRegistryEntry) -> Vec { let mut terms = Vec::new(); terms.push(entry.id.clone()); terms.push(entry.name.clone()); terms.push(entry.summary.clone()); terms.extend(entry.tags.iter().cloned()); terms.extend(entry.aliases.iter().cloned()); terms } fn normalize_for_match(value: &str) -> String { value .chars() .filter(|ch| !ch.is_whitespace() && *ch != '-' && *ch != '_' && *ch != ':') .flat_map(|ch| ch.to_lowercase()) .collect() }