//! Active skill awareness: explicit user intent. //! //! Active detection has the highest priority. It only fires when the user //! directly references a skill by slug, name, mention, or clear "use this" //! wording. use super::{SkillActivation, SkillContext, SkillEntry, normalize_skill_key}; use once_cell::sync::Lazy; use regex::Regex; #[derive(Debug, Clone, Default)] pub struct ActiveSkillAwareness; impl ActiveSkillAwareness { pub fn new() -> Self { Self } pub fn detect(&self, input: &str, skills: &[SkillEntry]) -> Option { let input_lower = input.to_lowercase(); self.match_by_prefix_pattern(&input_lower, skills) .or_else(|| self.match_by_name(&input_lower, skills)) .or_else(|| self.match_by_slug_substring(&input_lower, skills)) } fn match_by_prefix_pattern(&self, input: &str, skills: &[SkillEntry]) -> Option { static USE_PAT: Lazy = Lazy::new(|| Regex::new(r"(?i)^\s*(?:use|using|apply|with)\s+([a-z0-9/_-]+)").unwrap()); static SKILL_COLON_PAT: Lazy = Lazy::new(|| Regex::new(r"(?i)skill\s*:\s*([a-z0-9/_-]+)").unwrap()); static AT_PAT: Lazy = Lazy::new(|| Regex::new(r"@([a-z0-9][a-z0-9_/-]*[a-z0-9])").unwrap()); static ZH_PAT: Lazy = Lazy::new(|| { Regex::new( r"(?i)(?:使用|用|应用|启用|调用|帮我|帮忙|做一次|执行|进行)\s*([a-z0-9][a-z0-9_/\-]{0,60})", ) .unwrap() }); for pattern in [&USE_PAT, &SKILL_COLON_PAT, &AT_PAT, &ZH_PAT] { if let Some(caps) = pattern.captures(input) { let slug = caps.get(1)?.as_str().trim(); if let Some(skill) = self.find_skill_by_slug(slug, skills) { return Some(skill); } if let Some(skill) = self.find_skill_by_name(slug, skills) { return Some(skill); } } } None } fn match_by_name(&self, input: &str, skills: &[SkillEntry]) -> Option { for skill in skills { let name_lower = skill.name.to_lowercase(); let normalized_name = name_lower.replace(['-', '_'], " "); if input.contains(&name_lower) || input.contains(&normalized_name) { return Some(Self::context_from_skill(skill)); } } None } fn match_by_slug_substring(&self, input: &str, skills: &[SkillEntry]) -> Option { let cleaned = input .replace("please ", "") .replace("帮我", "") .replace("帮忙", "") .replace("使用", "") .replace("调用", "") .replace("启用", ""); for skill in skills { let slug = normalize_skill_key(&skill.slug); if cleaned.contains(&slug) || slug .split(['/', '-']) .any(|seg| seg.len() > 3 && cleaned.contains(seg)) { return Some(Self::context_from_skill(skill)); } } None } fn find_skill_by_slug(&self, slug: &str, skills: &[SkillEntry]) -> Option { let slug_key = normalize_skill_key(slug); skills .iter() .find(|s| normalize_skill_key(&s.slug) == slug_key) .map(Self::context_from_skill) } fn find_skill_by_name(&self, name: &str, skills: &[SkillEntry]) -> Option { let name_lower = name.to_lowercase().replace(['-', '_'], " "); skills .iter() .find(|s| s.name.to_lowercase().replace(['-', '_'], " ") == name_lower) .map(Self::context_from_skill) } fn context_from_skill(skill: &SkillEntry) -> SkillContext { SkillContext::new( skill, SkillActivation::Active, None, format!("# {} (actively invoked)\n\n{}", skill.name, skill.content), None, ) } }