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:
iven
2026-03-31 11:12:47 +08:00
parent d0ae7d2770
commit f79560a911
71 changed files with 8521 additions and 5997 deletions

View 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))
}