refactor(desktop): split kernel_commands/pipeline_commands into modules, add SaaS client libs and gateway modules
Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines) into focused sub-modules under kernel_commands/ and pipeline_commands/ directories. Add gateway module (commands, config, io, runtime), health_check, and 15 new TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel sub-systems (a2a, agent, chat, hands, skills, triggers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
350
desktop/src-tauri/src/kernel_commands/skill.rs
Normal file
350
desktop/src-tauri/src/kernel_commands/skill.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
//! 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<String>,
|
||||
pub tags: Vec<String>,
|
||||
pub mode: String,
|
||||
pub enabled: bool,
|
||||
pub triggers: Vec<String>,
|
||||
pub category: Option<String>,
|
||||
}
|
||||
|
||||
impl From<zclaw_skills::SkillManifest> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List all discovered skills
|
||||
///
|
||||
/// Returns skills from the Kernel's SkillRegistry.
|
||||
/// Skills are loaded from the skills/ directory during kernel initialization.
|
||||
#[tauri::command]
|
||||
pub async fn skill_list(
|
||||
state: State<'_, KernelState>,
|
||||
) -> Result<Vec<SkillInfoResponse>, 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.
|
||||
#[tauri::command]
|
||||
pub async fn skill_refresh(
|
||||
state: State<'_, KernelState>,
|
||||
skill_dir: Option<String>,
|
||||
) -> Result<Vec<SkillInfoResponse>, 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<String>,
|
||||
pub triggers: Vec<String>,
|
||||
pub actions: Vec<String>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Request body for updating a skill
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateSkillRequest {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub triggers: Option<Vec<String>>,
|
||||
pub actions: Option<Vec<String>>,
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
/// Create a new skill in the skills directory
|
||||
#[tauri::command]
|
||||
pub async fn skill_create(
|
||||
state: State<'_, KernelState>,
|
||||
request: CreateSkillRequest,
|
||||
) -> Result<SkillInfoResponse, String> {
|
||||
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,
|
||||
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
|
||||
#[tauri::command]
|
||||
pub async fn skill_update(
|
||||
state: State<'_, KernelState>,
|
||||
id: String,
|
||||
request: UpdateSkillRequest,
|
||||
) -> Result<SkillInfoResponse, 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())?;
|
||||
|
||||
// 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),
|
||||
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
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
impl From<SkillContext> 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<String>,
|
||||
pub duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<zclaw_skills::SkillResult> 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.
|
||||
#[tauri::command]
|
||||
pub async fn skill_execute(
|
||||
state: State<'_, KernelState>,
|
||||
id: String,
|
||||
context: SkillContext,
|
||||
input: serde_json::Value,
|
||||
autonomy_level: Option<String>,
|
||||
) -> Result<SkillResult, String> {
|
||||
// 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))
|
||||
}
|
||||
Reference in New Issue
Block a user