改进策划文件局部修改工具
支持同一文件多处独立替换 统一局部修改失败结果并补充测试
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
{"type":"function","function":{"name":"get_workflow_status","description":"读取当前策划工作流状态,只返回阶段列表、当前阶段、已批准阶段和待审批阶段;不推进阶段、不提交审批、不修改文件。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"list_resources","description":"列出固定资源的逻辑目录、资源 ID、标题和简介。资源是只读的随包文档;不要猜测物理路径。","parameters":{"type":"object","properties":{},"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"read_resource","description":"读取一份固定资源文档全文。每次读取一个 resource_id;资源只读。读到未实现占位文档时由你自行判断和处理。","parameters":{"type":"object","properties":{"resource_id":{"type":"string"}},"required":["resource_id"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件,优先用于已有文件的小范围修订。先读文件,以唯一且非空的 old_text 精确匹配并替换为 new_text;new_text 为空可删除片段,保留原文并追加可插入。匹配失败不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["path","old_text","new_text"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"patch_file","description":"局部修改 UTF-8 文件。使用 old_text/new_text,或使用 edits 一次进行多个独立替换;每个 old_text 必须非空且在原文件中唯一,匹配失败、重复或范围重叠时不修改文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"},"old_text":{"type":"string"},"new_text":{"type":"string"},"edits":{"type":"array","items":{"type":"object","properties":{"old_text":{"type":"string"},"new_text":{"type":"string"}},"required":["old_text","new_text"],"additionalProperties":false}}},"required":["path"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"delete_path","description":"谨慎使用;永久删除工作区内的文件或目录;目录会连同全部内容递归删除,不备份。先确认目标及删除范围。path 使用相对路径,不能删除工作区根目录,也不能经过链接。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"list_dir","description":"列出工作目录内的文件和目录。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
|
||||
{"type":"function","function":{"name":"read_file","description":"读取工作目录内的 UTF-8 文本文件。path 使用相对路径。","parameters":{"type":"object","properties":{"path":{"type":"string"}},"required":["path"],"additionalProperties":false}}},
|
||||
|
||||
@@ -264,22 +264,45 @@ pub(crate) fn execute_design_file_tool(
|
||||
}
|
||||
"patch_file" => {
|
||||
let relative = required_tool_path(args)?;
|
||||
let old = args
|
||||
.get("old_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 old_text")?;
|
||||
let new = args
|
||||
.get("new_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 new_text")?;
|
||||
if old.is_empty() {
|
||||
return Err("old_text 不能为空".to_string());
|
||||
}
|
||||
let edits = if let Some(items) = args.get("edits").and_then(Value::as_array) {
|
||||
if items.is_empty() {
|
||||
return Err("edits 不能为空".to_string());
|
||||
}
|
||||
items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, item)| {
|
||||
let old = item
|
||||
.get("old_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| format!("edits[{index}].old_text 必须是字符串"))?;
|
||||
let new = item
|
||||
.get("new_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| format!("edits[{index}].new_text 必须是字符串"))?;
|
||||
if old.is_empty() {
|
||||
return Err(format!("edits[{index}].old_text 不能为空"));
|
||||
}
|
||||
Ok((old.to_string(), new.to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, String>>()?
|
||||
} else {
|
||||
let old = args
|
||||
.get("old_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 old_text")?;
|
||||
let new = args
|
||||
.get("new_text")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("缺少 new_text")?;
|
||||
if old.is_empty() {
|
||||
return Err("old_text 不能为空".to_string());
|
||||
}
|
||||
vec![(old.to_string(), new.to_string())]
|
||||
};
|
||||
let (display, path) = resolve_design_workspace_path(root, &relative)?;
|
||||
if !path.is_file() {
|
||||
return Ok(Value::String(format!(
|
||||
"局部修改失败:文件不存在:{display}"
|
||||
)));
|
||||
return Err(format!("文件不存在:{display}"));
|
||||
}
|
||||
let content =
|
||||
fs::read_to_string(&path).map_err(|error| format!("读取失败:{error}"))?;
|
||||
@@ -288,20 +311,52 @@ pub(crate) fn execute_design_file_tool(
|
||||
} else {
|
||||
"\n"
|
||||
};
|
||||
let old = old.replace("\r\n", "\n").replace('\n', newline);
|
||||
let new = new.replace("\r\n", "\n").replace('\n', newline);
|
||||
let count = content.matches(&old).count();
|
||||
if count != 1 {
|
||||
return Err(format!(
|
||||
"原文匹配 {count} 处,需要唯一匹配;请重新读取文件并扩大匹配范围"
|
||||
));
|
||||
let normalized = edits
|
||||
.into_iter()
|
||||
.map(|(old, new)| {
|
||||
(
|
||||
old.replace("\r\n", "\n").replace('\n', newline),
|
||||
new.replace("\r\n", "\n").replace('\n', newline),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
for (index, (old, new)) in normalized.iter().enumerate() {
|
||||
let count = content.matches(old).count();
|
||||
if count == 0 {
|
||||
return Err(format!("edits[{index}] 原文未找到:{display}"));
|
||||
}
|
||||
if count != 1 {
|
||||
return Err(format!(
|
||||
"edits[{index}] 原文匹配 {count} 处,必须唯一:{display}"
|
||||
));
|
||||
}
|
||||
let start = content.find(old).expect("count checked");
|
||||
let end = start + old.len();
|
||||
if let Some((other_index, _other_start, _other_end)) = matches
|
||||
.iter()
|
||||
.find(|(_, other_start, other_end)| start < *_other_end && *_other_start < end)
|
||||
{
|
||||
return Err(format!(
|
||||
"edits[{index}] 与 edits[{other_index}] 修改范围重叠:{display}"
|
||||
));
|
||||
}
|
||||
matches.push((index, start, end));
|
||||
let _ = new;
|
||||
}
|
||||
crate::write_game_creator_private_file(
|
||||
&path,
|
||||
content.replacen(&old, &new, 1).as_bytes(),
|
||||
"策划工作区文件",
|
||||
)?;
|
||||
Ok(Value::String(format!("已局部修改 {display}")))
|
||||
let mut updated = content.clone();
|
||||
for (index, start, end) in matches.into_iter().rev() {
|
||||
let (_, new) = &normalized[index];
|
||||
updated.replace_range(start..end, new);
|
||||
}
|
||||
if updated == content {
|
||||
return Err(format!("没有产生修改:{display}"));
|
||||
}
|
||||
crate::write_game_creator_private_file(&path, updated.as_bytes(), "策划工作区文件")?;
|
||||
Ok(Value::String(format!(
|
||||
"已局部修改 {display}({} 处)",
|
||||
normalized.len()
|
||||
)))
|
||||
}
|
||||
"delete_path" => {
|
||||
let relative = required_tool_path(args)?;
|
||||
@@ -645,6 +700,29 @@ mod tests {
|
||||
)
|
||||
.expect("patch");
|
||||
assert!(patched.as_str().unwrap().contains("已局部修改"));
|
||||
execute_design_file_tool(
|
||||
root,
|
||||
"write_file",
|
||||
&json!({"path":"notes/multi.md","content":"甲\n乙\n丙"}),
|
||||
)
|
||||
.expect("write multi");
|
||||
let multi = execute_design_file_tool(
|
||||
root,
|
||||
"patch_file",
|
||||
&json!({
|
||||
"path":"notes/multi.md",
|
||||
"edits":[
|
||||
{"old_text":"甲","new_text":"一"},
|
||||
{"old_text":"丙","new_text":"三"}
|
||||
]
|
||||
}),
|
||||
)
|
||||
.expect("multi patch");
|
||||
assert!(multi.as_str().unwrap().contains("2 处"));
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join("design_artifacts/notes/multi.md")).expect("read multi"),
|
||||
"一\n乙\n三"
|
||||
);
|
||||
execute_design_file_tool(root, "delete_path", &json!({"path":"notes"}))
|
||||
.expect("delete dir");
|
||||
assert!(!root.join("design_artifacts/notes").exists());
|
||||
|
||||
Reference in New Issue
Block a user