focus/crates/focus-tools/src/write.rs

145 lines
4.5 KiB
Rust

//! `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<PathBuf>) -> 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<ToolResult> {
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"
);
}
}