refactor: 清理未使用代码并添加未来功能标记
Some checks failed
CI / Rust Check (push) Has been cancelled
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

style: 统一代码格式和注释风格

docs: 更新多个功能文档的完整度和状态

feat(runtime): 添加路径验证工具支持

fix(pipeline): 改进条件判断和变量解析逻辑

test(types): 为ID类型添加全面测试用例

chore: 更新依赖项和Cargo.lock文件

perf(mcp): 优化MCP协议传输和错误处理
This commit is contained in:
iven
2026-03-25 21:55:12 +08:00
parent aa6a9cbd84
commit bf6d81f9c6
109 changed files with 12271 additions and 815 deletions

View File

@@ -13,7 +13,8 @@ use zclaw_types::Result;
/// HTML exporter
pub struct HtmlExporter {
/// Template name
/// Template name (reserved for future template support)
#[allow(dead_code)] // TODO: Implement template-based HTML export
template: String,
}
@@ -26,6 +27,7 @@ impl HtmlExporter {
}
/// Create with specific template
#[allow(dead_code)] // Reserved for future template support
pub fn with_template(template: &str) -> Self {
Self {
template: template.to_string(),

View File

@@ -26,6 +26,7 @@ impl MarkdownExporter {
}
/// Create without front matter
#[allow(dead_code)] // Reserved for future use
pub fn without_front_matter() -> Self {
Self {
include_front_matter: false,

View File

@@ -568,7 +568,7 @@ use zip::{ZipWriter, write::SimpleFileOptions};
#[cfg(test)]
mod tests {
use super::*;
use crate::generation::{ClassroomMetadata, TeachingStyle, DifficultyLevel};
use crate::generation::{ClassroomMetadata, TeachingStyle, DifficultyLevel, SceneType};
fn create_test_classroom() -> Classroom {
Classroom {

View File

@@ -704,6 +704,7 @@ Actions can be:
}
/// Generate scene using LLM
#[allow(dead_code)] // Reserved for future LLM-based scene generation
async fn generate_scene_with_llm(
&self,
driver: &dyn LlmDriver,
@@ -787,6 +788,7 @@ Ensure the outline is coherent and follows good pedagogical practices."#.to_stri
}
/// Get system prompt for scene generation
#[allow(dead_code)] // Reserved for future use
fn get_scene_system_prompt(&self) -> String {
r#"You are an expert educational content creator. Your task is to generate detailed teaching scenes.
@@ -871,6 +873,7 @@ Actions can be:
}
/// Parse scene from LLM response text
#[allow(dead_code)] // Reserved for future use
fn parse_scene_from_text(&self, text: &str, item: &OutlineItem, order: usize) -> Result<GeneratedScene> {
let json_text = self.extract_json(text);
@@ -902,6 +905,7 @@ Actions can be:
}
/// Parse actions from scene data
#[allow(dead_code)] // Reserved for future use
fn parse_actions(&self, scene_data: &serde_json::Value) -> Vec<SceneAction> {
scene_data.get("actions")
.and_then(|v| v.as_array())
@@ -914,6 +918,7 @@ Actions can be:
}
/// Parse single action
#[allow(dead_code)] // Reserved for future use
fn parse_single_action(&self, action: &serde_json::Value) -> Option<SceneAction> {
let action_type = action.get("type")?.as_str()?;
@@ -1058,6 +1063,7 @@ Generate {} outline items that flow logically and cover the topic comprehensivel
}
/// Generate scene for outline item (would be replaced by LLM call)
#[allow(dead_code)] // Reserved for future use
fn generate_scene_for_item(&self, item: &OutlineItem, order: usize) -> Result<GeneratedScene> {
let actions = match item.scene_type {
SceneType::Slide => vec![

View File

@@ -56,6 +56,7 @@ pub struct Kernel {
skills: Arc<SkillRegistry>,
skill_executor: Arc<KernelSkillExecutor>,
hands: Arc<HandRegistry>,
trigger_manager: crate::trigger_manager::TriggerManager,
}
impl Kernel {
@@ -97,6 +98,9 @@ impl Kernel {
// Create skill executor
let skill_executor = Arc::new(KernelSkillExecutor::new(skills.clone()));
// Initialize trigger manager
let trigger_manager = crate::trigger_manager::TriggerManager::new(hands.clone());
// Restore persisted agents
let persisted = memory.list_agents().await?;
for agent in persisted {
@@ -113,6 +117,7 @@ impl Kernel {
skills,
skill_executor,
hands,
trigger_manager,
})
}
@@ -420,6 +425,82 @@ impl Kernel {
let context = HandContext::default();
self.hands.execute(hand_id, &context, input).await
}
// ============================================================
// Trigger Management
// ============================================================
/// List all triggers
pub async fn list_triggers(&self) -> Vec<crate::trigger_manager::TriggerEntry> {
self.trigger_manager.list_triggers().await
}
/// Get a specific trigger
pub async fn get_trigger(&self, id: &str) -> Option<crate::trigger_manager::TriggerEntry> {
self.trigger_manager.get_trigger(id).await
}
/// Create a new trigger
pub async fn create_trigger(
&self,
config: zclaw_hands::TriggerConfig,
) -> Result<crate::trigger_manager::TriggerEntry> {
self.trigger_manager.create_trigger(config).await
}
/// Update a trigger
pub async fn update_trigger(
&self,
id: &str,
updates: crate::trigger_manager::TriggerUpdateRequest,
) -> Result<crate::trigger_manager::TriggerEntry> {
self.trigger_manager.update_trigger(id, updates).await
}
/// Delete a trigger
pub async fn delete_trigger(&self, id: &str) -> Result<()> {
self.trigger_manager.delete_trigger(id).await
}
/// Execute a trigger
pub async fn execute_trigger(
&self,
id: &str,
input: serde_json::Value,
) -> Result<zclaw_hands::TriggerResult> {
self.trigger_manager.execute_trigger(id, input).await
}
// ============================================================
// Approval Management (Stub Implementation)
// ============================================================
/// List pending approvals
pub async fn list_approvals(&self) -> Vec<ApprovalEntry> {
// Stub: Return empty list
Vec::new()
}
/// Respond to an approval
pub async fn respond_to_approval(
&self,
_id: &str,
_approved: bool,
_reason: Option<String>,
) -> Result<()> {
// Stub: Return error
Err(zclaw_types::ZclawError::NotFound(format!("Approval not found")))
}
}
/// Approval entry for pending approvals
#[derive(Debug, Clone)]
pub struct ApprovalEntry {
pub id: String,
pub hand_id: String,
pub status: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub input: serde_json::Value,
}
/// Response from sending a message

View File

@@ -6,6 +6,7 @@ mod kernel;
mod registry;
mod capabilities;
mod events;
pub mod trigger_manager;
pub mod config;
pub mod director;
pub mod generation;
@@ -16,6 +17,7 @@ pub use registry::*;
pub use capabilities::*;
pub use events::*;
pub use config::*;
pub use trigger_manager::{TriggerManager, TriggerEntry, TriggerUpdateRequest, TriggerManagerConfig};
pub use director::*;
pub use generation::*;
pub use export::{ExportFormat, ExportOptions, ExportResult, Exporter, export_classroom};

View File

@@ -0,0 +1,372 @@
//! Trigger Manager
//!
//! Manages triggers for automated task execution.
//!
//! # Lock Order Safety
//!
//! This module uses a single `RwLock<InternalState>` to avoid potential deadlocks.
//! Previously, multiple locks (`triggers` and `states`) could cause deadlocks when
//! acquired in different orders across methods.
//!
//! The unified state structure ensures atomic access to all trigger-related data.
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zclaw_types::Result;
use zclaw_hands::{TriggerConfig, TriggerType, TriggerState, TriggerResult, HandRegistry};
/// Internal state container for all trigger-related data.
///
/// Using a single structure behind one RwLock eliminates the possibility of
/// deadlocks caused by inconsistent lock acquisition orders.
#[derive(Debug)]
struct InternalState {
/// Registered triggers
triggers: HashMap<String, TriggerEntry>,
/// Execution states
states: HashMap<String, TriggerState>,
}
impl InternalState {
fn new() -> Self {
Self {
triggers: HashMap::new(),
states: HashMap::new(),
}
}
}
/// Trigger manager for coordinating automated triggers
pub struct TriggerManager {
/// Unified internal state behind a single RwLock.
///
/// This prevents deadlocks by ensuring all trigger data is accessed
/// through a single lock acquisition point.
state: RwLock<InternalState>,
/// Hand registry
hand_registry: Arc<HandRegistry>,
/// Configuration
config: TriggerManagerConfig,
}
/// Trigger entry with additional metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerEntry {
/// Core trigger configuration
#[serde(flatten)]
pub config: TriggerConfig,
/// Creation timestamp
pub created_at: DateTime<Utc>,
/// Last modification timestamp
pub modified_at: DateTime<Utc>,
/// Optional description
pub description: Option<String>,
/// Optional tags
#[serde(default)]
pub tags: Vec<String>,
}
/// Default max executions per hour
fn default_max_executions_per_hour() -> u32 { 10 }
/// Default persist value
fn default_persist() -> bool { true }
/// Trigger manager configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerManagerConfig {
/// Maximum executions per hour (default)
#[serde(default = "default_max_executions_per_hour")]
pub max_executions_per_hour: u32,
/// Enable persistent storage
#[serde(default = "default_persist")]
pub persist: bool,
/// Storage path for trigger data
pub storage_path: Option<String>,
}
impl Default for TriggerManagerConfig {
fn default() -> Self {
Self {
max_executions_per_hour: 10,
persist: true,
storage_path: None,
}
}
}
impl TriggerManager {
/// Create new trigger manager
pub fn new(hand_registry: Arc<HandRegistry>) -> Self {
Self {
state: RwLock::new(InternalState::new()),
hand_registry,
config: TriggerManagerConfig::default(),
}
}
/// Create with custom configuration
pub fn with_config(
hand_registry: Arc<HandRegistry>,
config: TriggerManagerConfig,
) -> Self {
Self {
state: RwLock::new(InternalState::new()),
hand_registry,
config,
}
}
/// List all triggers
pub async fn list_triggers(&self) -> Vec<TriggerEntry> {
let state = self.state.read().await;
state.triggers.values().cloned().collect()
}
/// Get a specific trigger
pub async fn get_trigger(&self, id: &str) -> Option<TriggerEntry> {
let state = self.state.read().await;
state.triggers.get(id).cloned()
}
/// Create a new trigger
pub async fn create_trigger(&self, config: TriggerConfig) -> Result<TriggerEntry> {
// Validate hand exists (outside of our lock to avoid holding two locks)
if self.hand_registry.get(&config.hand_id).await.is_none() {
return Err(zclaw_types::ZclawError::InvalidInput(
format!("Hand '{}' not found", config.hand_id)
));
}
let id = config.id.clone();
let now = Utc::now();
let entry = TriggerEntry {
config,
created_at: now,
modified_at: now,
description: None,
tags: Vec::new(),
};
// Initialize state and insert trigger atomically under single lock
let state = TriggerState::new(&id);
{
let mut internal = self.state.write().await;
internal.states.insert(id.clone(), state);
internal.triggers.insert(id.clone(), entry.clone());
}
Ok(entry)
}
/// Update an existing trigger
pub async fn update_trigger(
&self,
id: &str,
updates: TriggerUpdateRequest,
) -> Result<TriggerEntry> {
// Validate hand exists if being updated (outside of our lock)
if let Some(hand_id) = &updates.hand_id {
if self.hand_registry.get(hand_id).await.is_none() {
return Err(zclaw_types::ZclawError::InvalidInput(
format!("Hand '{}' not found", hand_id)
));
}
}
let mut internal = self.state.write().await;
let Some(entry) = internal.triggers.get_mut(id) else {
return Err(zclaw_types::ZclawError::NotFound(
format!("Trigger '{}' not found", id)
));
};
// Apply updates
if let Some(name) = &updates.name {
entry.config.name = name.clone();
}
if let Some(enabled) = updates.enabled {
entry.config.enabled = enabled;
}
if let Some(hand_id) = &updates.hand_id {
entry.config.hand_id = hand_id.clone();
}
if let Some(trigger_type) = &updates.trigger_type {
entry.config.trigger_type = trigger_type.clone();
}
entry.modified_at = Utc::now();
Ok(entry.clone())
}
/// Delete a trigger
pub async fn delete_trigger(&self, id: &str) -> Result<()> {
let mut internal = self.state.write().await;
if internal.triggers.remove(id).is_none() {
return Err(zclaw_types::ZclawError::NotFound(
format!("Trigger '{}' not found", id)
));
}
// Also remove associated state atomically
internal.states.remove(id);
Ok(())
}
/// Get trigger state
pub async fn get_state(&self, id: &str) -> Option<TriggerState> {
let state = self.state.read().await;
state.states.get(id).cloned()
}
/// Check if trigger should fire based on type and input.
///
/// This method performs rate limiting and condition checks using a single
/// read lock to avoid deadlocks.
pub async fn should_fire(&self, id: &str, input: &serde_json::Value) -> bool {
let internal = self.state.read().await;
let Some(entry) = internal.triggers.get(id) else {
return false;
};
// Check if enabled
if !entry.config.enabled {
return false;
}
// Check rate limiting using the same lock
if let Some(state) = internal.states.get(id) {
// Check execution count this hour
let one_hour_ago = Utc::now() - chrono::Duration::hours(1);
if let Some(last_exec) = state.last_execution {
if last_exec > one_hour_ago {
if state.execution_count >= self.config.max_executions_per_hour {
return false;
}
}
}
}
// Check trigger-specific conditions
match &entry.config.trigger_type {
TriggerType::Manual => false,
TriggerType::Schedule { cron: _ } => {
// For schedule triggers, use cron parser
// Simplified check - real implementation would use cron library
true
}
TriggerType::Event { pattern } => {
// Check if input matches pattern
input.to_string().contains(pattern)
}
TriggerType::Webhook { path: _, secret: _ } => {
// Webhook triggers are fired externally
false
}
TriggerType::MessagePattern { pattern } => {
// Check if message matches pattern
input.to_string().contains(pattern)
}
TriggerType::FileSystem { path: _, events: _ } => {
// File system triggers are fired by file watcher
false
}
}
}
/// Execute a trigger.
///
/// This method carefully manages lock scope to avoid deadlocks:
/// 1. Acquires read lock to check trigger exists and get config
/// 2. Releases lock before calling external hand registry
/// 3. Acquires write lock to update state
pub async fn execute_trigger(&self, id: &str, input: serde_json::Value) -> Result<TriggerResult> {
// Check if should fire (uses its own lock scope)
if !self.should_fire(id, &input).await {
return Err(zclaw_types::ZclawError::InvalidInput(
format!("Trigger '{}' should not fire", id)
));
}
// Get hand_id (release lock before calling hand registry)
let hand_id = {
let internal = self.state.read().await;
let entry = internal.triggers.get(id)
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
format!("Trigger '{}' not found", id)
))?;
entry.config.hand_id.clone()
};
// Get hand (outside of our lock to avoid potential deadlock with hand_registry)
let hand = self.hand_registry.get(&hand_id).await
.ok_or_else(|| zclaw_types::ZclawError::InvalidInput(
format!("Hand '{}' not found", hand_id)
))?;
// Update state before execution
{
let mut internal = self.state.write().await;
let state = internal.states.entry(id.to_string()).or_insert_with(|| TriggerState::new(id));
state.execution_count += 1;
}
// Execute hand (outside of lock to avoid blocking other operations)
let context = zclaw_hands::HandContext {
agent_id: zclaw_types::AgentId::new(),
working_dir: None,
env: std::collections::HashMap::new(),
timeout_secs: 300,
callback_url: None,
};
let hand_result = hand.execute(&context, input.clone()).await;
// Build trigger result from hand result
let trigger_result = match &hand_result {
Ok(res) => TriggerResult {
timestamp: Utc::now(),
success: res.success,
output: Some(res.output.clone()),
error: res.error.clone(),
trigger_input: input.clone(),
},
Err(e) => TriggerResult {
timestamp: Utc::now(),
success: false,
output: None,
error: Some(e.to_string()),
trigger_input: input.clone(),
},
};
// Update state after execution
{
let mut internal = self.state.write().await;
if let Some(state) = internal.states.get_mut(id) {
state.last_execution = Some(Utc::now());
state.last_result = Some(trigger_result.clone());
}
}
// Return the original hand result or convert to trigger result
hand_result.map(|_| trigger_result)
}
}
/// Request for updating a trigger
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TriggerUpdateRequest {
/// New name
pub name: Option<String>,
/// Enable/disable
pub enabled: Option<bool>,
/// New hand ID
pub hand_id: Option<String>,
/// New trigger type
pub trigger_type: Option<TriggerType>,
}