rucora_tools/file/
edit.rs1use async_trait::async_trait;
6use rucora_core::{
7 error::ToolError,
8 tool::{Tool, ToolCategory},
9};
10use serde_json::{Value, json};
11use std::path::PathBuf;
12
13use super::FileToolConfig;
14
15pub struct FileEditTool {
35 config: FileToolConfig,
36}
37
38impl FileEditTool {
39 pub fn new() -> Self {
41 Self {
42 config: FileToolConfig::new(),
43 }
44 }
45
46 pub fn with_allowed_dirs(self, dirs: Vec<PathBuf>) -> Self {
48 Self {
49 config: self.config.with_allowed_dirs(dirs),
50 }
51 }
52
53 pub fn with_max_file_size(self, size: u64) -> Self {
55 Self {
56 config: self.config.with_max_file_size(size),
57 }
58 }
59}
60
61impl Default for FileEditTool {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67#[async_trait]
68impl Tool for FileEditTool {
69 fn name(&self) -> &str {
71 "file_edit"
72 }
73
74 fn description(&self) -> Option<&str> {
76 Some("通过精确字符串替换编辑文件内容(有安全限制)")
77 }
78
79 fn categories(&self) -> &'static [ToolCategory] {
81 &[ToolCategory::File]
82 }
83
84 fn input_schema(&self) -> Value {
86 json!({
87 "type": "object",
88 "properties": {
89 "path": {
90 "type": "string",
91 "description": "文件路径"
92 },
93 "old_string": {
94 "type": "string",
95 "description": "要查找并替换的精确文本(必须在文件中精确出现一次)"
96 },
97 "new_string": {
98 "type": "string",
99 "description": "替换后的文本(空字符串表示删除)"
100 }
101 },
102 "required": ["path", "old_string", "new_string"]
103 })
104 }
105
106 async fn call(&self, input: Value) -> Result<Value, ToolError> {
108 let path_str = input
109 .get("path")
110 .and_then(|v| v.as_str())
111 .ok_or_else(|| ToolError::Message("缺少必需的 'path' 字段".to_string()))?;
112
113 let old_string = input
114 .get("old_string")
115 .and_then(|v| v.as_str())
116 .ok_or_else(|| ToolError::Message("缺少必需的 'old_string' 字段".to_string()))?;
117
118 let new_string = input
119 .get("new_string")
120 .and_then(|v| v.as_str())
121 .ok_or_else(|| ToolError::Message("缺少必需的 'new_string' 字段".to_string()))?;
122
123 if old_string.is_empty() {
124 return Err(ToolError::Message("old_string 不能为空".to_string()));
125 }
126
127 let path = self.config.validate_path_for_read(path_str)?;
128
129 let content = tokio::fs::read_to_string(&path)
131 .await
132 .map_err(|e| ToolError::Message(format!("读取文件失败:{e}")))?;
133
134 let matches = content.matches(old_string).count();
136 if matches == 0 {
137 return Err(ToolError::Message(format!("未找到匹配文本:{old_string}")));
138 }
139 if matches > 1 {
140 return Err(ToolError::Message(format!(
141 "找到 {matches} 处匹配,匹配歧义。请提供更精确的唯一匹配文本"
142 )));
143 }
144
145 let new_content = content.replacen(old_string, new_string, 1);
147
148 self.config
150 .check_file_size(new_content.len() as u64, "编辑后文件")?;
151
152 tokio::fs::write(&path, new_content)
154 .await
155 .map_err(|e| ToolError::Message(format!("写入文件失败:{e}")))?;
156
157 Ok(json!({
158 "success": true,
159 "path": path_str,
160 "replacements": 1
161 }))
162 }
163}