//! `write` 工具:整文件覆盖写入,自动创建父目录。 //! The `write` tool: whole-file overwrite, auto-creating parent directories. use crate::resolve_path; use focus_core::model::ContentBlock; use focus_core::tool::{Tool, ToolEffects, ToolResult, ToolUpdateSink}; use focus_core::{CoreError, CoreResult}; use focus_json::JsonValue; use std::path::PathBuf; /// 覆盖写入文件的工具。 /// A tool that overwrites files. #[derive(Debug, Clone)] pub struct WriteTool { root: PathBuf, } impl WriteTool { /// 以项目根目录构造工具。 /// Build the tool with a project root. pub fn new(root: impl Into) -> Self { Self { root: root.into() } } } impl Tool for WriteTool { fn name(&self) -> &str { "write" } fn description(&self) -> &str { "Write text to a file, creating parent directories as needed. \ Overwrites any existing content." } fn parameters(&self) -> JsonValue { let mut o = JsonValue::obj(); o.insert("type", "object".into()).ok(); let mut props = JsonValue::obj(); let mut path = JsonValue::obj(); path.insert("type", "string".into()).ok(); props.insert("path", path).ok(); let mut content = JsonValue::obj(); content.insert("type", "string".into()).ok(); props.insert("content", content).ok(); o.insert("properties", props).ok(); let mut required = JsonValue::arr(); required.push("path".into()).ok(); required.push("content".into()).ok(); o.insert("required", required).ok(); o } fn effects(&self) -> ToolEffects { ToolEffects::WRITE } fn execute( &self, _id: &str, args: &JsonValue, _on_update: Option<&ToolUpdateSink>, ) -> CoreResult { let path = args .get_str("path") .ok_or_else(|| CoreError::InvalidArgs("write: missing 'path'".into()))?; let content = args .get_str("content") .ok_or_else(|| CoreError::InvalidArgs("write: missing 'content'".into()))?; let resolved = resolve_path(&self.root, path); if let Some(parent) = resolved.parent() { std::fs::create_dir_all(parent).map_err(|e| { CoreError::Tool(format!( "write {}: create parent dirs: {}", resolved.display(), e )) })?; } std::fs::write(&resolved, content.as_bytes()) .map_err(|e| CoreError::Tool(format!("write {}: {}", resolved.display(), e)))?; let mut details = JsonValue::obj(); details .insert("path", resolved.display().to_string().into()) .ok(); details.insert("bytes", (content.len() as f64).into()).ok(); Ok(ToolResult { content: vec![ContentBlock::text(format!( "Wrote {} bytes to {}", content.len(), resolved.display() ))], details, added_tool_names: Vec::new(), terminate: false, }) } } #[cfg(test)] mod tests { use super::*; use std::fs; fn temp_dir(tag: &str) -> PathBuf { let d = std::env::temp_dir().join(format!("focus-tools-write-{}-{}", tag, std::process::id())); let _ = fs::remove_dir_all(&d); fs::create_dir_all(&d).unwrap(); d } #[test] fn writes_and_overwrites() { let dir = temp_dir("overwrite"); fs::write(dir.join("a.txt"), "old").unwrap(); let tool = WriteTool::new(&dir); let mut args = JsonValue::obj(); args.insert("path", "a.txt".into()).ok(); args.insert("content", "new content".into()).ok(); let r = tool.execute("id", &args, None).unwrap(); assert!(r.content[0].as_text().unwrap().text.contains("11 bytes")); assert_eq!( fs::read_to_string(dir.join("a.txt")).unwrap(), "new content" ); } #[test] fn creates_parent_directories() { let dir = temp_dir("nested"); let tool = WriteTool::new(&dir); let mut args = JsonValue::obj(); args.insert("path", "deep/nested/file.txt".into()).ok(); args.insert("content", "hi".into()).ok(); tool.execute("id", &args, None).unwrap(); assert_eq!( fs::read_to_string(dir.join("deep/nested/file.txt")).unwrap(), "hi" ); } }