Files
zclaw_openfang/crates/zclaw-skills/src/runner.rs
iven a71c4138cc
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
fix(audit): 修复深度审计发现的 P0/P1 问题 (8项)
基于 DEEP_AUDIT_REPORT.md 修复 2 CRITICAL + 4 HIGH + 1 MEDIUM 问题:

- C1: PromptOnly 技能集成 LLM 调用 — 定义 LlmCompleter trait,
  通过 LlmDriverAdapter 桥接 zclaw_runtime::LlmDriver,
  PromptOnlySkill.execute() 现在调用 LLM 生成内容
- C2: 反思引擎空记忆 bug — 新增 query_memories_for_reflection()
  从 VikingStorage 查询真实记忆传入 reflect()
- H7: Agent Store 接口适配 — KernelClient 添加 listClones/createClone/
  deleteClone/updateClone 方法,映射到 agent_* 命令
- H8: Hand 审批检查 — hand_execute 执行前检查 needs_approval,
  需审批返回 pending_approval 状态
- M1: 幽灵命令注册 — 注册 hand_get/hand_run_status/hand_run_list
  三个 Tauri 桩命令
- H1/H2: SpeechHand/TwitterHand 添加 demo 标签
- H5: 归档过时 VERIFICATION_REPORT

文档更新: DEEP_AUDIT_REPORT.md 标记修复状态,README.md 更新
关键指标和变更历史。整体完成度从 ~50% 提升至 ~58%。
2026-03-27 09:36:50 +08:00

172 lines
5.4 KiB
Rust

//! Skill runners for different execution modes
use async_trait::async_trait;
use serde_json::Value;
use std::process::Command;
use std::time::Instant;
use tracing::warn;
use zclaw_types::Result;
use super::{Skill, SkillContext, SkillManifest, SkillResult};
/// Prompt-only skill execution
pub struct PromptOnlySkill {
manifest: SkillManifest,
prompt_template: String,
}
impl PromptOnlySkill {
pub fn new(manifest: SkillManifest, prompt_template: String) -> Self {
Self { manifest, prompt_template }
}
fn format_prompt(&self, input: &Value) -> String {
let mut prompt = self.prompt_template.clone();
if let Value::String(s) = input {
prompt = prompt.replace("{{input}}", s);
} else {
prompt = prompt.replace("{{input}}", &serde_json::to_string_pretty(input).unwrap_or_default());
}
prompt
}
}
#[async_trait]
impl Skill for PromptOnlySkill {
fn manifest(&self) -> &SkillManifest {
&self.manifest
}
async fn execute(&self, context: &SkillContext, input: Value) -> Result<SkillResult> {
let prompt = self.format_prompt(&input);
// If an LLM completer is available, generate an AI response
if let Some(completer) = &context.llm {
match completer.complete(&prompt).await {
Ok(response) => return Ok(SkillResult::success(Value::String(response))),
Err(e) => {
warn!("[PromptOnlySkill] LLM completion failed: {}, falling back to raw prompt", e);
// Fall through to return raw prompt
}
}
}
// No LLM available — return formatted prompt (backward compatible)
Ok(SkillResult::success(Value::String(prompt)))
}
}
/// Python script skill execution
pub struct PythonSkill {
manifest: SkillManifest,
script_path: std::path::PathBuf,
}
impl PythonSkill {
pub fn new(manifest: SkillManifest, script_path: std::path::PathBuf) -> Self {
Self { manifest, script_path }
}
}
#[async_trait]
impl Skill for PythonSkill {
fn manifest(&self) -> &SkillManifest {
&self.manifest
}
async fn execute(&self, context: &SkillContext, input: Value) -> Result<SkillResult> {
let start = Instant::now();
let input_json = serde_json::to_string(&input).unwrap_or_default();
let output = Command::new("python3")
.arg(&self.script_path)
.env("SKILL_INPUT", &input_json)
.env("AGENT_ID", &context.agent_id)
.env("SESSION_ID", &context.session_id)
.output()
.map_err(|e| zclaw_types::ZclawError::ToolError(format!("Failed to execute Python: {}", e)))?;
let duration_ms = start.elapsed().as_millis() as u64;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let result = serde_json::from_str(&stdout)
.map(|v| SkillResult {
success: true,
output: v,
error: None,
duration_ms: Some(duration_ms),
tokens_used: None,
})
.unwrap_or_else(|_| SkillResult::success(Value::String(stdout.to_string())));
Ok(result)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(SkillResult::error(stderr))
}
}
}
/// Shell command skill execution
pub struct ShellSkill {
manifest: SkillManifest,
command: String,
}
impl ShellSkill {
pub fn new(manifest: SkillManifest, command: String) -> Self {
Self { manifest, command }
}
}
#[async_trait]
impl Skill for ShellSkill {
fn manifest(&self) -> &SkillManifest {
&self.manifest
}
async fn execute(&self, context: &SkillContext, input: Value) -> Result<SkillResult> {
let start = Instant::now();
let mut cmd = self.command.clone();
if let Value::String(s) = input {
// Shell-quote the input to prevent command injection
let quoted = shlex::try_quote(&s)
.map_err(|_| zclaw_types::ZclawError::ToolError(
"Input contains null bytes and cannot be safely quoted".to_string()
))?;
cmd = cmd.replace("{{input}}", &quoted);
}
#[cfg(target_os = "windows")]
let output = {
Command::new("cmd")
.args(["/C", &cmd])
.current_dir(context.working_dir.as_ref().unwrap_or(&std::path::PathBuf::from(".")))
.output()
.map_err(|e| zclaw_types::ZclawError::ToolError(format!("Failed to execute shell: {}", e)))?
};
#[cfg(not(target_os = "windows"))]
let output = {
Command::new("sh")
.args(["-c", &cmd])
.current_dir(context.working_dir.as_ref().unwrap_or(&std::path::PathBuf::from(".")))
.output()
.map_err(|e| zclaw_types::ZclawError::ToolError(format!("Failed to execute shell: {}", e)))?
};
let _duration_ms = start.elapsed().as_millis() as u64;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(SkillResult::success(Value::String(stdout.to_string())))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(SkillResult::error(stderr))
}
}
}