feat: P0 KernelClient功能修复 + P1/P2/P3质量改进
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P0 KernelClient 功能断裂修复: - Skill CUD: registry.rs create/update/delete + serialize_skill_md + kernel proxy - Workflow CUD: pipeline_commands.rs create/update/delete + serde_yaml依赖 - Agent更新: registry update方法 + AgentConfigUpdated事件 + agent_update命令 - Hand流式事件: HandStart/HandEnd变体替换ToolStart/ToolEnd - 后端验证: hand_get/hand_run_status/hand_run_list确认实现完整 - Approval闭环: respond_to_approval后台spawn+5分钟超时轮询 P2/P3 质量改进: - Browser WebDriver: TCP探测ChromeDriver/GeckoDriver/Edge端口替换硬编码true - api-fallbacks: 移除假技能和16个捏造安全层,替换为真实能力映射 - dead_code清理: 移除5个模块级#![allow(dead_code)],删除3个真正死方法, 删除未注册的compactor_compact_llm命令,warnings从8降到3 - 所有变更通过cargo check + tsc --noEmit验证
This commit is contained in:
@@ -157,11 +157,22 @@ impl BrowserHand {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if WebDriver is available
|
||||
/// Check if WebDriver is available by probing common ports
|
||||
fn check_webdriver(&self) -> bool {
|
||||
// Check if ChromeDriver or GeckoDriver is running
|
||||
// For now, return true as the actual check would require network access
|
||||
true
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
|
||||
// Probe default WebDriver ports: ChromeDriver (9515), GeckoDriver (4444), Edge (17556)
|
||||
let ports = [9515, 4444, 17556];
|
||||
for port in ports {
|
||||
let addr = format!("127.0.0.1:{}", port);
|
||||
if let Ok(addr) = addr.parse() {
|
||||
if TcpStream::connect_timeout(&addr, Duration::from_millis(500)).is_ok() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -480,6 +480,35 @@ impl Kernel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update an existing agent's configuration
|
||||
pub async fn update_agent(&self, config: AgentConfig) -> Result<()> {
|
||||
let id = config.id;
|
||||
|
||||
// Validate the agent exists
|
||||
if self.registry.get(&id).is_none() {
|
||||
return Err(zclaw_types::ZclawError::NotFound(
|
||||
format!("Agent not found: {}", id)
|
||||
));
|
||||
}
|
||||
|
||||
// Validate capabilities
|
||||
self.capabilities.validate(&config.capabilities)?;
|
||||
|
||||
// Save updated config to memory
|
||||
self.memory.save_agent(&config).await?;
|
||||
|
||||
// Update in registry (preserves state and message count)
|
||||
self.registry.update(config.clone());
|
||||
|
||||
// Emit event
|
||||
self.events.publish(Event::AgentConfigUpdated {
|
||||
agent_id: id,
|
||||
name: config.name.clone(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all agents
|
||||
pub fn list_agents(&self) -> Vec<AgentInfo> {
|
||||
self.registry.list()
|
||||
@@ -710,6 +739,42 @@ impl Kernel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the configured skills directory
|
||||
pub fn skills_dir(&self) -> Option<&std::path::PathBuf> {
|
||||
self.config.skills_dir.as_ref()
|
||||
}
|
||||
|
||||
/// Create a new skill in the skills directory
|
||||
pub async fn create_skill(&self, manifest: zclaw_skills::SkillManifest) -> Result<()> {
|
||||
let skills_dir = self.config.skills_dir.as_ref()
|
||||
.ok_or_else(|| zclaw_types::ZclawError::InvalidInput(
|
||||
"Skills directory not configured".into()
|
||||
))?;
|
||||
self.skills.create_skill(skills_dir, manifest).await
|
||||
}
|
||||
|
||||
/// Update an existing skill
|
||||
pub async fn update_skill(
|
||||
&self,
|
||||
id: &zclaw_types::SkillId,
|
||||
manifest: zclaw_skills::SkillManifest,
|
||||
) -> Result<zclaw_skills::SkillManifest> {
|
||||
let skills_dir = self.config.skills_dir.as_ref()
|
||||
.ok_or_else(|| zclaw_types::ZclawError::InvalidInput(
|
||||
"Skills directory not configured".into()
|
||||
))?;
|
||||
self.skills.update_skill(skills_dir, id, manifest).await
|
||||
}
|
||||
|
||||
/// Delete a skill
|
||||
pub async fn delete_skill(&self, id: &zclaw_types::SkillId) -> Result<()> {
|
||||
let skills_dir = self.config.skills_dir.as_ref()
|
||||
.ok_or_else(|| zclaw_types::ZclawError::InvalidInput(
|
||||
"Skills directory not configured".into()
|
||||
))?;
|
||||
self.skills.delete_skill(skills_dir, id).await
|
||||
}
|
||||
|
||||
/// Execute a skill with the given ID and input
|
||||
pub async fn execute_skill(
|
||||
&self,
|
||||
|
||||
@@ -38,6 +38,12 @@ impl AgentRegistry {
|
||||
self.message_counts.remove(id);
|
||||
}
|
||||
|
||||
/// Update an agent's configuration (preserves state and message count)
|
||||
pub fn update(&self, config: AgentConfig) {
|
||||
let id = config.id;
|
||||
self.agents.insert(id, config);
|
||||
}
|
||||
|
||||
/// Get an agent by ID
|
||||
pub fn get(&self, id: &AgentId) -> Option<AgentConfig> {
|
||||
self.agents.get(id).map(|r| r.clone())
|
||||
|
||||
@@ -171,6 +171,150 @@ impl SkillRegistry {
|
||||
skills.insert(manifest.id.clone(), skill);
|
||||
manifests.insert(manifest.id.clone(), manifest);
|
||||
}
|
||||
|
||||
/// Create a skill from manifest, writing SKILL.md to disk
|
||||
pub async fn create_skill(
|
||||
&self,
|
||||
skills_dir: &std::path::Path,
|
||||
manifest: SkillManifest,
|
||||
) -> Result<()> {
|
||||
let skill_dir = skills_dir.join(manifest.id.as_str());
|
||||
if skill_dir.exists() {
|
||||
return Err(zclaw_types::ZclawError::InvalidInput(
|
||||
format!("Skill directory already exists: {}", skill_dir.display())
|
||||
));
|
||||
}
|
||||
|
||||
// Create directory
|
||||
std::fs::create_dir_all(&skill_dir)
|
||||
.map_err(|e| zclaw_types::ZclawError::StorageError(
|
||||
format!("Failed to create skill directory: {}", e)
|
||||
))?;
|
||||
|
||||
// Write SKILL.md
|
||||
let content = serialize_skill_md(&manifest);
|
||||
std::fs::write(skill_dir.join("SKILL.md"), &content)
|
||||
.map_err(|e| zclaw_types::ZclawError::StorageError(
|
||||
format!("Failed to write SKILL.md: {}", e)
|
||||
))?;
|
||||
|
||||
// Load into registry
|
||||
self.load_skill_from_dir(&skill_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a skill manifest, rewriting SKILL.md on disk
|
||||
pub async fn update_skill(
|
||||
&self,
|
||||
skills_dir: &std::path::Path,
|
||||
id: &SkillId,
|
||||
updates: SkillManifest,
|
||||
) -> Result<SkillManifest> {
|
||||
// Find existing skill directory
|
||||
let skill_dir = skills_dir.join(id.as_str());
|
||||
if !skill_dir.exists() {
|
||||
return Err(zclaw_types::ZclawError::NotFound(
|
||||
format!("Skill directory not found: {}", skill_dir.display())
|
||||
));
|
||||
}
|
||||
|
||||
// Merge: start from existing manifest, apply updates
|
||||
let existing = self.get_manifest(id).await
|
||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(
|
||||
format!("Skill not found in registry: {}", id)
|
||||
))?;
|
||||
|
||||
let updated = SkillManifest {
|
||||
id: existing.id.clone(),
|
||||
name: if updates.name.is_empty() { existing.name } else { updates.name },
|
||||
description: if updates.description.is_empty() { existing.description } else { updates.description },
|
||||
version: if updates.version.is_empty() { existing.version } else { updates.version },
|
||||
author: updates.author.or(existing.author),
|
||||
mode: existing.mode,
|
||||
capabilities: if updates.capabilities.is_empty() { existing.capabilities } else { updates.capabilities },
|
||||
input_schema: updates.input_schema.or(existing.input_schema),
|
||||
output_schema: updates.output_schema.or(existing.output_schema),
|
||||
tags: if updates.tags.is_empty() { existing.tags } else { updates.tags },
|
||||
category: updates.category.or(existing.category),
|
||||
triggers: if updates.triggers.is_empty() { existing.triggers } else { updates.triggers },
|
||||
enabled: updates.enabled,
|
||||
};
|
||||
|
||||
// Rewrite SKILL.md
|
||||
let content = serialize_skill_md(&updated);
|
||||
std::fs::write(skill_dir.join("SKILL.md"), &content)
|
||||
.map_err(|e| zclaw_types::ZclawError::StorageError(
|
||||
format!("Failed to write SKILL.md: {}", e)
|
||||
))?;
|
||||
|
||||
// Reload into registry
|
||||
self.remove(id).await;
|
||||
self.load_skill_from_dir(&skill_dir).await?;
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Delete a skill: remove directory from disk and unregister
|
||||
pub async fn delete_skill(
|
||||
&self,
|
||||
skills_dir: &std::path::Path,
|
||||
id: &SkillId,
|
||||
) -> Result<()> {
|
||||
let skill_dir = skills_dir.join(id.as_str());
|
||||
if skill_dir.exists() {
|
||||
std::fs::remove_dir_all(&skill_dir)
|
||||
.map_err(|e| zclaw_types::ZclawError::StorageError(
|
||||
format!("Failed to remove skill directory: {}", e)
|
||||
))?;
|
||||
}
|
||||
|
||||
self.remove(id).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize a SkillManifest into SKILL.md frontmatter format
|
||||
fn serialize_skill_md(manifest: &SkillManifest) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
// Frontmatter
|
||||
parts.push("---".to_string());
|
||||
parts.push(format!("name: \"{}\"", manifest.name));
|
||||
parts.push(format!("description: \"{}\"", manifest.description));
|
||||
parts.push(format!("version: \"{}\"", manifest.version));
|
||||
parts.push(format!("mode: {}", match manifest.mode {
|
||||
SkillMode::PromptOnly => "prompt-only",
|
||||
SkillMode::Python => "python",
|
||||
SkillMode::Shell => "shell",
|
||||
SkillMode::Wasm => "wasm",
|
||||
SkillMode::Native => "native",
|
||||
}));
|
||||
if !manifest.capabilities.is_empty() {
|
||||
parts.push(format!("capabilities: {}", manifest.capabilities.join(", ")));
|
||||
}
|
||||
if !manifest.tags.is_empty() {
|
||||
parts.push(format!("tags: {}", manifest.tags.join(", ")));
|
||||
}
|
||||
if !manifest.triggers.is_empty() {
|
||||
parts.push("triggers:".to_string());
|
||||
for trigger in &manifest.triggers {
|
||||
parts.push(format!(" - \"{}\"", trigger));
|
||||
}
|
||||
}
|
||||
if let Some(ref cat) = manifest.category {
|
||||
parts.push(format!("category: \"{}\"", cat));
|
||||
}
|
||||
parts.push(format!("enabled: {}", manifest.enabled));
|
||||
parts.push("---".to_string());
|
||||
parts.push(String::new());
|
||||
|
||||
// Body: use description as the skill content
|
||||
parts.push(format!("# {}", manifest.name));
|
||||
parts.push(String::new());
|
||||
parts.push(manifest.description.clone());
|
||||
|
||||
parts.join("\n")
|
||||
}
|
||||
|
||||
impl Default for SkillRegistry {
|
||||
|
||||
@@ -32,6 +32,12 @@ pub enum Event {
|
||||
new_state: String,
|
||||
},
|
||||
|
||||
/// Agent configuration updated
|
||||
AgentConfigUpdated {
|
||||
agent_id: AgentId,
|
||||
name: String,
|
||||
},
|
||||
|
||||
/// Session created
|
||||
SessionCreated {
|
||||
session_id: SessionId,
|
||||
@@ -145,6 +151,7 @@ impl Event {
|
||||
Event::AgentSpawned { .. } => "agent_spawned",
|
||||
Event::AgentTerminated { .. } => "agent_terminated",
|
||||
Event::AgentStateChanged { .. } => "agent_state_changed",
|
||||
Event::AgentConfigUpdated { .. } => "agent_config_updated",
|
||||
Event::SessionCreated { .. } => "session_created",
|
||||
Event::MessageReceived { .. } => "message_received",
|
||||
Event::MessageSent { .. } => "message_sent",
|
||||
|
||||
Reference in New Issue
Block a user