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

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:
iven
2026-03-30 10:55:08 +08:00
parent d345e60a6a
commit 813b49a986
19 changed files with 951 additions and 102 deletions

View File

@@ -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
}
}

View File

@@ -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,

View File

@@ -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())

View File

@@ -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 {

View File

@@ -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",