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
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:
@@ -1,10 +1,13 @@
|
||||
//! File write tool
|
||||
//! File write tool with path validation
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
use zclaw_types::{Result, ZclawError};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::tool::{Tool, ToolContext};
|
||||
use super::path_validator::PathValidator;
|
||||
|
||||
pub struct FileWriteTool;
|
||||
|
||||
@@ -21,7 +24,7 @@ impl Tool for FileWriteTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Write content to a file on the filesystem"
|
||||
"Write content to a file on the filesystem. The file must be within allowed paths."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
@@ -35,22 +38,92 @@ impl Tool for FileWriteTool {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The content to write to the file"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"description": "Write mode: 'create' (fail if exists), 'overwrite' (replace), 'append' (add to end)",
|
||||
"enum": ["create", "overwrite", "append"],
|
||||
"default": "create"
|
||||
},
|
||||
"encoding": {
|
||||
"type": "string",
|
||||
"description": "Content encoding (default: utf-8)",
|
||||
"enum": ["utf-8", "base64"]
|
||||
}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, input: Value, _context: &ToolContext) -> Result<Value> {
|
||||
let _path = input["path"].as_str()
|
||||
async fn execute(&self, input: Value, context: &ToolContext) -> Result<Value> {
|
||||
let path = input["path"].as_str()
|
||||
.ok_or_else(|| ZclawError::InvalidInput("Missing 'path' parameter".into()))?;
|
||||
|
||||
let content = input["content"].as_str()
|
||||
.ok_or_else(|| ZclawError::InvalidInput("Missing 'content' parameter".into()))?;
|
||||
|
||||
// TODO: Implement actual file writing with path validation
|
||||
let mode = input["mode"].as_str().unwrap_or("create");
|
||||
let encoding = input["encoding"].as_str().unwrap_or("utf-8");
|
||||
|
||||
// Validate path using context's path validator or create default
|
||||
let validator = context.path_validator.as_ref()
|
||||
.map(|v| v.clone())
|
||||
.unwrap_or_else(|| {
|
||||
// Create default validator with workspace as allowed path
|
||||
let mut validator = PathValidator::new();
|
||||
if let Some(ref workspace) = context.working_directory {
|
||||
validator = validator.with_workspace(std::path::PathBuf::from(workspace));
|
||||
}
|
||||
validator
|
||||
});
|
||||
|
||||
// Validate path for write access
|
||||
let validated_path = validator.validate_write(path)?;
|
||||
|
||||
// Decode content based on encoding
|
||||
let bytes = match encoding {
|
||||
"base64" => {
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
BASE64.decode(content)
|
||||
.map_err(|e| ZclawError::InvalidInput(format!("Invalid base64 content: {}", e)))?
|
||||
}
|
||||
_ => content.as_bytes().to_vec()
|
||||
};
|
||||
|
||||
// Check if file exists and handle mode
|
||||
let file_exists = validated_path.exists();
|
||||
|
||||
if file_exists && mode == "create" {
|
||||
return Err(ZclawError::InvalidInput(format!(
|
||||
"File already exists: {}",
|
||||
validated_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
// Write file
|
||||
let mut file = match mode {
|
||||
"append" => {
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&validated_path)
|
||||
.map_err(|e| ZclawError::ToolError(format!("Failed to open file for append: {}", e)))?
|
||||
}
|
||||
_ => {
|
||||
// create or overwrite
|
||||
fs::File::create(&validated_path)
|
||||
.map_err(|e| ZclawError::ToolError(format!("Failed to create file: {}", e)))?
|
||||
}
|
||||
};
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|e| ZclawError::ToolError(format!("Failed to write file: {}", e)))?;
|
||||
|
||||
Ok(json!({
|
||||
"success": true,
|
||||
"bytes_written": content.len()
|
||||
"bytes_written": bytes.len(),
|
||||
"path": validated_path.to_string_lossy(),
|
||||
"mode": mode
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -60,3 +133,85 @@ impl Default for FileWriteTool {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
use crate::tool::builtin::PathValidator;
|
||||
|
||||
fn create_test_context_with_tempdir(dir: &std::path::Path) -> ToolContext {
|
||||
// Use canonicalized path to handle Windows extended-length paths
|
||||
let workspace = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
|
||||
let path_validator = Some(PathValidator::new().with_workspace(workspace));
|
||||
ToolContext {
|
||||
agent_id: zclaw_types::AgentId::new(),
|
||||
working_directory: None,
|
||||
session_id: None,
|
||||
skill_executor: None,
|
||||
path_validator,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_new_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("test.txt").to_str().unwrap().to_string();
|
||||
|
||||
let input = json!({
|
||||
"path": path,
|
||||
"content": "Hello, World!"
|
||||
});
|
||||
|
||||
let context = create_test_context_with_tempdir(dir.path());
|
||||
|
||||
let tool = FileWriteTool::new();
|
||||
let result = tool.execute(input, &context).await.unwrap();
|
||||
|
||||
assert!(result["success"].as_bool().unwrap());
|
||||
assert_eq!(result["bytes_written"].as_u64().unwrap(), 13);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_mode_fails_on_existing() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("existing.txt");
|
||||
fs::write(&path, "existing content").unwrap();
|
||||
|
||||
let input = json!({
|
||||
"path": path.to_str().unwrap(),
|
||||
"content": "new content",
|
||||
"mode": "create"
|
||||
});
|
||||
|
||||
let context = create_test_context_with_tempdir(dir.path());
|
||||
|
||||
let tool = FileWriteTool::new();
|
||||
let result = tool.execute(input, &context).await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_overwrite_mode() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("test.txt");
|
||||
fs::write(&path, "old content").unwrap();
|
||||
|
||||
let input = json!({
|
||||
"path": path.to_str().unwrap(),
|
||||
"content": "new content",
|
||||
"mode": "overwrite"
|
||||
});
|
||||
|
||||
let context = create_test_context_with_tempdir(dir.path());
|
||||
|
||||
let tool = FileWriteTool::new();
|
||||
let result = tool.execute(input, &context).await.unwrap();
|
||||
|
||||
assert!(result["success"].as_bool().unwrap());
|
||||
|
||||
let content = fs::read_to_string(&path).unwrap();
|
||||
assert_eq!(content, "new content");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user