mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(mcp): resolve generated file paths containing spaces (#5819)
* fix(mcp): resolve generated file paths containing spaces * fix(mcp): avoid partial rewrites at whitespace suffixes --------- Co-authored-by: xbzz1018 <186267668+xbzz1018@users.noreply.github.com>
This commit is contained in:
parent
df63139321
commit
f83bc05a5f
@ -288,6 +288,50 @@ def _rewrite_unique_bare_filenames(
|
||||
return rewritten
|
||||
|
||||
|
||||
def _rewrite_changed_paths_with_spaces(
|
||||
text: str,
|
||||
*,
|
||||
changed_files: Iterable[Path],
|
||||
thread_id: str,
|
||||
user_id: str,
|
||||
source_base_dir: Path | None,
|
||||
) -> str:
|
||||
"""Rewrite changed paths with spaces only at unambiguous text boundaries."""
|
||||
candidates: dict[str, set[str]] = {}
|
||||
for path in changed_files:
|
||||
spellings = [str(path)]
|
||||
if path.anchor == "/":
|
||||
# Some servers print file URIs without percent-encoding spaces.
|
||||
spellings.extend((f"file:{path}", f"file://{path}"))
|
||||
if source_base_dir is not None:
|
||||
try:
|
||||
relative = path.relative_to(source_base_dir).as_posix()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
spellings.extend((relative, f"./{relative}"))
|
||||
matching_spellings = [spelling for spelling in spellings if any(char.isspace() for char in spelling) and spelling in text]
|
||||
if not matching_spellings:
|
||||
continue
|
||||
virtual_path = _local_uri_to_virtual_path(str(path), thread_id=thread_id, user_id=user_id)
|
||||
if virtual_path is None:
|
||||
continue
|
||||
for spelling in matching_spellings:
|
||||
candidates.setdefault(spelling, set()).add(virtual_path)
|
||||
|
||||
rewritten = text
|
||||
for spelling in sorted(candidates, key=len, reverse=True):
|
||||
destinations = candidates[spelling]
|
||||
if len(destinations) != 1 or spelling not in rewritten:
|
||||
continue
|
||||
# Whitespace after an unquoted spelling may continue a longer filename.
|
||||
# Require punctuation or the end of the text instead of rewriting a prefix.
|
||||
pattern = re.compile(rf"(?<![\w./\\-]){re.escape(spelling)}(?!(?:[\w\s/\\-]|\.[\w]))")
|
||||
replacement = next(iter(destinations))
|
||||
rewritten = pattern.sub(lambda _match: replacement, rewritten)
|
||||
return rewritten
|
||||
|
||||
|
||||
def _rewrite_local_paths_in_text(
|
||||
text: str,
|
||||
*,
|
||||
@ -327,6 +371,16 @@ def _rewrite_local_paths_in_text(
|
||||
return token
|
||||
return f"{rewritten}{trailing}"
|
||||
|
||||
if changed_files is not None:
|
||||
changed_files = list(changed_files)
|
||||
text = _rewrite_changed_paths_with_spaces(
|
||||
text,
|
||||
changed_files=changed_files,
|
||||
thread_id=thread_id,
|
||||
user_id=user_id,
|
||||
source_base_dir=source_base_dir,
|
||||
)
|
||||
|
||||
rewritten = _LOCAL_PATH_IN_TEXT_RE.sub(_replace, text)
|
||||
if changed_files is None:
|
||||
return rewritten
|
||||
|
||||
@ -212,6 +212,125 @@ class TestRewriteLocalPathsInText:
|
||||
|
||||
assert result == f"Screenshot saved to {VIRTUAL_PATH_PREFIX}/workspace/absolute-output/page.png"
|
||||
|
||||
def test_new_absolute_path_with_spaces_is_rewritten(self, paths: Paths):
|
||||
workspace = paths.sandbox_work_dir("t1", user_id="u1")
|
||||
src = _workspace_file(paths, "reports/final report.txt")
|
||||
text = f'Saved as "{src}" and ready.'
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
text,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
source_base_dir=workspace,
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == f'Saved as "{VIRTUAL_PATH_PREFIX}/workspace/reports/final report.txt" and ready.'
|
||||
|
||||
def test_new_relative_path_with_spaces_is_rewritten(self, paths: Paths):
|
||||
workspace = paths.sandbox_work_dir("t1", user_id="u1")
|
||||
src = _workspace_file(paths, "final reports/result.txt")
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
"Saved as final reports/result.txt.",
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
source_base_dir=workspace,
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/final reports/result.txt."
|
||||
|
||||
def test_new_dot_relative_path_with_spaces_is_rewritten(self, paths: Paths):
|
||||
workspace = paths.sandbox_work_dir("t1", user_id="u1")
|
||||
src = _workspace_file(paths, "final reports/result.txt")
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
"Saved as ./final reports/result.txt.",
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
source_base_dir=workspace,
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/final reports/result.txt."
|
||||
|
||||
def test_changed_path_with_spaces_outside_user_data_is_untouched(self, tmp_path: Path, paths: Paths):
|
||||
outside = tmp_path / "outside report.txt"
|
||||
outside.write_text("outside", encoding="utf-8")
|
||||
text = f"Saved as {outside}"
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
text,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
changed_files=[outside],
|
||||
)
|
||||
|
||||
assert result == text
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="exercises POSIX file URI spellings")
|
||||
def test_new_file_uri_with_literal_spaces_is_rewritten(self, paths: Paths):
|
||||
src = _workspace_file(paths, "final report.txt")
|
||||
text = f"Saved as file://{src}"
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
text,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/final report.txt"
|
||||
|
||||
def test_changed_path_with_spaces_does_not_rewrite_longer_name(self, paths: Paths):
|
||||
src = _workspace_file(paths, "final report.txt")
|
||||
text = f"Backup: {src}.bak"
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
text,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == text
|
||||
|
||||
def test_changed_path_with_spaces_does_not_rewrite_whitespace_suffix(self, paths: Paths):
|
||||
src = _workspace_file(paths, "final report.txt")
|
||||
longer = _workspace_file(paths, "final report.txt copy")
|
||||
text = f"Backup: {longer}"
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
text,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == text
|
||||
|
||||
def test_changed_path_with_spaces_followed_by_prose_is_untouched(self, paths: Paths):
|
||||
src = _workspace_file(paths, "final report.txt")
|
||||
text = f"Saved as {src} and ready."
|
||||
|
||||
with _patch_paths(paths):
|
||||
result = mcp_tools._rewrite_local_paths_in_text(
|
||||
text,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert result == text
|
||||
|
||||
def test_tmpdir_output_under_workspace_is_rewritten(self, paths: Paths):
|
||||
src = _workspace_file(paths, ".mcp/tmp/page.png")
|
||||
text = f"Saved to {src}"
|
||||
@ -629,6 +748,22 @@ class TestConvertCallToolResultRewrites:
|
||||
|
||||
assert content[0]["text"] == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/page-2026.yml"
|
||||
|
||||
def test_text_path_with_spaces_rewritten_from_changed_files(self, paths: Paths):
|
||||
workspace = paths.sandbox_work_dir("t1", user_id="u1")
|
||||
src = _workspace_file(paths, "report with spaces.txt")
|
||||
result = CallToolResult(content=[TextContent(type="text", text=f"Saved as {src}")], isError=False)
|
||||
|
||||
with _patch_paths(paths):
|
||||
content, _ = mcp_tools._convert_call_tool_result(
|
||||
result,
|
||||
thread_id="t1",
|
||||
user_id="u1",
|
||||
source_base_dir=workspace,
|
||||
changed_files=[src],
|
||||
)
|
||||
|
||||
assert content[0]["text"] == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/report with spaces.txt"
|
||||
|
||||
def test_no_context_does_not_rewrite(self, paths: Paths):
|
||||
src = _workspace_file(paths, "x.png", content=b"png")
|
||||
uri = src.as_uri()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user