145 lines
4.5 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|