//! Skill CRUD + execute commands //! //! Skills are loaded from the Kernel's SkillRegistry. //! Skills are registered during kernel initialization. use std::path::PathBuf; use serde::{Deserialize, Serialize}; use serde_json; use tauri::State; use zclaw_types::SkillId; use super::{validate_id, KernelState}; use crate::intelligence::validation::validate_identifier; // ============================================================================ // Skills Commands - Dynamic Discovery // ============================================================================ /// Skill information response for frontend #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SkillInfoResponse { pub id: String, pub name: String, pub description: String, pub version: String, pub capabilities: Vec, pub tags: Vec, pub mode: String, pub enabled: bool, pub triggers: Vec, pub category: Option, #[serde(default = "default_source")] pub source: String, #[serde(default)] pub path: Option, } fn default_source() -> String { "builtin".to_string() } impl From for SkillInfoResponse { fn from(manifest: zclaw_skills::SkillManifest) -> Self { Self { id: manifest.id.to_string(), name: manifest.name, description: manifest.description, version: manifest.version, capabilities: manifest.capabilities, tags: manifest.tags, mode: format!("{:?}", manifest.mode), enabled: manifest.enabled, triggers: manifest.triggers, category: manifest.category, source: "builtin".to_string(), path: None, } } } /// List all discovered skills /// /// Returns skills from the Kernel's SkillRegistry. /// Skills are loaded from the skills/ directory during kernel initialization. // @connected #[tauri::command] pub async fn skill_list( state: State<'_, KernelState>, ) -> Result, String> { let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let skills = kernel.list_skills().await; println!("[skill_list] Found {} skills", skills.len()); for skill in &skills { println!("[skill_list] - {} ({})", skill.name, skill.id); } Ok(skills.into_iter().map(SkillInfoResponse::from).collect()) } /// Refresh skills from a directory /// /// Re-scans the skills directory for new or updated skills. /// Optionally accepts a custom directory path to scan. // @connected #[tauri::command] pub async fn skill_refresh( state: State<'_, KernelState>, skill_dir: Option, ) -> Result, String> { let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; // Convert optional string to PathBuf let dir_path = skill_dir.map(PathBuf::from); // Refresh skills kernel.refresh_skills(dir_path) .await .map_err(|e| format!("Failed to refresh skills: {}", e))?; // Return updated list let skills = kernel.list_skills().await; Ok(skills.into_iter().map(SkillInfoResponse::from).collect()) } // ============================================================================ // Skill CRUD Commands // ============================================================================ /// Request body for creating a new skill #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreateSkillRequest { pub name: String, pub description: Option, pub triggers: Vec, pub actions: Vec, pub enabled: Option, } /// Request body for updating a skill #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UpdateSkillRequest { pub name: Option, pub description: Option, pub triggers: Option>, pub actions: Option>, pub enabled: Option, } /// Create a new skill in the skills directory // @connected #[tauri::command] pub async fn skill_create( state: State<'_, KernelState>, request: CreateSkillRequest, ) -> Result { let name = request.name.trim().to_string(); if name.is_empty() { return Err("Skill name cannot be empty".to_string()); } // Generate skill ID from name let id = name.to_lowercase() .replace(' ', "-") .replace(|c: char| !c.is_alphanumeric() && c != '-', ""); validate_identifier(&id, "skill_id") .map_err(|e| e.to_string())?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; let manifest = zclaw_skills::SkillManifest { id: SkillId::new(&id), name: name.clone(), description: request.description.unwrap_or_default(), version: "1.0.0".to_string(), author: None, mode: zclaw_skills::SkillMode::PromptOnly, capabilities: request.actions, input_schema: None, output_schema: None, tags: vec![], category: None, triggers: request.triggers, tools: vec![], // P2-19: Include tools field enabled: request.enabled.unwrap_or(true), }; kernel.create_skill(manifest.clone()) .await .map_err(|e| format!("Failed to create skill: {}", e))?; Ok(SkillInfoResponse::from(manifest)) } /// Update an existing skill // @connected #[tauri::command] pub async fn skill_update( state: State<'_, KernelState>, id: String, request: UpdateSkillRequest, ) -> Result { validate_identifier(&id, "skill_id") .map_err(|e| e.to_string())?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; // Get existing manifest let existing = kernel.skills() .get_manifest(&SkillId::new(&id)) .await .ok_or_else(|| format!("Skill not found: {}", id))?; // Build updated manifest from existing + request fields let updated = zclaw_skills::SkillManifest { id: existing.id.clone(), name: request.name.unwrap_or(existing.name), description: request.description.unwrap_or(existing.description), version: existing.version.clone(), author: existing.author.clone(), mode: existing.mode.clone(), capabilities: request.actions.unwrap_or(existing.capabilities), input_schema: existing.input_schema.clone(), output_schema: existing.output_schema.clone(), tags: existing.tags.clone(), category: existing.category.clone(), triggers: request.triggers.unwrap_or(existing.triggers), tools: existing.tools.clone(), // P2-19: Preserve tools on update enabled: request.enabled.unwrap_or(existing.enabled), }; let result = kernel.update_skill(&SkillId::new(&id), updated) .await .map_err(|e| format!("Failed to update skill: {}", e))?; Ok(SkillInfoResponse::from(result)) } /// Delete a skill // @connected #[tauri::command] pub async fn skill_delete( state: State<'_, KernelState>, id: String, ) -> Result<(), String> { validate_identifier(&id, "skill_id") .map_err(|e| e.to_string())?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; kernel.delete_skill(&SkillId::new(&id)) .await .map_err(|e| format!("Failed to delete skill: {}", e))?; Ok(()) } // ============================================================================ // Skill Execution Command // ============================================================================ /// Skill execution context #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SkillContext { pub agent_id: String, pub session_id: String, pub working_dir: Option, } impl From for zclaw_skills::SkillContext { fn from(ctx: SkillContext) -> Self { Self { agent_id: ctx.agent_id, session_id: ctx.session_id, working_dir: ctx.working_dir.map(std::path::PathBuf::from), env: std::collections::HashMap::new(), timeout_secs: 300, network_allowed: true, file_access_allowed: true, llm: None, // Injected by Kernel.execute_skill() } } } /// Skill execution result #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SkillResult { pub success: bool, pub output: serde_json::Value, pub error: Option, pub duration_ms: Option, } impl From for SkillResult { fn from(result: zclaw_skills::SkillResult) -> Self { Self { success: result.success, output: result.output, error: result.error, duration_ms: result.duration_ms, } } } /// Execute a skill /// /// Executes a skill with the given ID and input. /// Returns the skill result as JSON. // @connected #[tauri::command] pub async fn skill_execute( state: State<'_, KernelState>, id: String, context: SkillContext, input: serde_json::Value, autonomy_level: Option, ) -> Result { // Validate skill ID let id = validate_id(&id, "skill_id")?; let kernel_lock = state.lock().await; let kernel = kernel_lock.as_ref() .ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?; // Autonomy guard: supervised mode creates an approval request for ALL skills if autonomy_level.as_deref() == Some("supervised") { let approval = kernel.create_approval(id.clone(), input).await; return Ok(SkillResult { success: false, output: serde_json::json!({ "status": "pending_approval", "approval_id": approval.id, "skill_id": approval.hand_id, "message": "监督模式下所有技能执行需要用户审批" }), error: None, duration_ms: None, }); } // Assisted mode: require approval for non-prompt skills (shell/python) that have side effects if autonomy_level.as_deref() != Some("autonomous") { let skill_id = SkillId::new(&id); if let Some(manifest) = kernel.skills().get_manifest(&skill_id).await { match manifest.mode { zclaw_skills::SkillMode::Shell | zclaw_skills::SkillMode::Python => { let approval = kernel.create_approval(id.clone(), input).await; return Ok(SkillResult { success: false, output: serde_json::json!({ "status": "pending_approval", "approval_id": approval.id, "skill_id": approval.hand_id, "message": format!("技能 '{}' 使用 {:?} 模式,需要用户审批后执行", manifest.name, manifest.mode) }), error: None, duration_ms: None, }); } _ => {} // PromptOnly and other modes are safe to execute directly } } } // Execute skill directly let result = kernel.execute_skill(&id, context.into(), input).await .map_err(|e| format!("Failed to execute skill: {}", e))?; Ok(SkillResult::from(result)) }