mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(client): preserve existing Markdown when publishing upload companions (#7054)
* fix(client): preserve existing Markdown when publishing upload companions * test(client): explain running-loop conversion coverage * ci: upgrade Playwright to 1.62.1 for Ubuntu 26.04 Backport the already-merged upstream #7219 fix to unblock the older #7054 replay workflow, which still locks Playwright 1.59.1. (cherry picked from commit e790e7e6bab37c6e61d61f36c65f15d79a8655f8) --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
60940ae9c5
commit
3ccc876493
@ -2456,7 +2456,7 @@ Content-less sub-agent final messages report `No response generated` instead of
|
||||
|
||||
An ordinary `task` also receives a defensive snapshot of the dispatching run's current uploads. This lets eligible sub-agents use `list_uploaded_files` to find earlier-turn files without returning same-turn attachments as historical. Delayed or recovered `batch_task` workers leave this tool disabled because they have no valid turn-local upload boundary.
|
||||
|
||||
Embedded Python uploads use the same captured content for the saved document and its Markdown conversion, even if the caller's source changes during upload.
|
||||
Embedded Python uploads use the same captured content for the saved document and its Markdown conversion, even if the caller's source changes during upload. Converted Markdown receives a free filename with a numeric suffix on collision, preserving user-authored Markdown and companions from earlier uploads. The upload response reports the actual companion filename.
|
||||
|
||||
历史上传发现支持稳定续页:`list_uploaded_files` 默认每页 20 项、最多 100 项,
|
||||
将返回的 `next_cursor` 作为下一次调用的 `cursor`,保留相同过滤条件,直到末页不再
|
||||
|
||||
@ -1873,11 +1873,10 @@ class DeerFlowClient:
|
||||
# copy, not write_bytes: the companion keeps the
|
||||
# converter's permissions, so a sandbox running as
|
||||
# another uid can still read it.
|
||||
if unique_md_name in existing_names:
|
||||
md_path = copy_upload_file_no_symlink(uploads_dir, unique_md_name, converted)
|
||||
else:
|
||||
md_path = publish_upload_copy(uploads_dir, unique_md_name, converted, claim_seed=provisional_md_name, seen_names=seen_names)
|
||||
unique_md_name = md_path.name
|
||||
# Existing Markdown may be user-authored or owned
|
||||
# by another document from an earlier upload call.
|
||||
md_path = publish_upload_copy(uploads_dir, unique_md_name, converted, claim_seed=provisional_md_name, seen_names=seen_names)
|
||||
unique_md_name = md_path.name
|
||||
except UnsafeUploadPathError:
|
||||
logger.warning("Skipping markdown companion with unsafe destination: %s", unique_md_name)
|
||||
md_path = None
|
||||
|
||||
@ -19,9 +19,11 @@ metadata; every attempt reads the held private capture. Replacement copies share
|
||||
this capture helper. Never copy snapshot permission bits onto the saved upload.
|
||||
Close the caller's descriptor after capture and finish conversion workers before
|
||||
removing captures. Never reopen the caller/sandbox upload path for conversion.
|
||||
Names already present in the call's initial regular-file snapshot retain
|
||||
replacement behavior; these replacements are not serialized. Apply source
|
||||
timestamps after new-file publication so cleanup cannot sweep an in-flight copy
|
||||
Original upload names already present in the call's initial regular-file snapshot
|
||||
retain replacement behavior; these replacements are not serialized. Converted
|
||||
Markdown always uses no-replace publication, including across upload calls, and
|
||||
registers the actual saved name. An existing basename does not establish companion
|
||||
ownership. Apply source timestamps after new-file publication so cleanup cannot sweep an in-flight copy
|
||||
of an old source. New final names must expose complete copies.
|
||||
|
||||
No-replace publication binds pre-link staging and post-link destination checks to
|
||||
|
||||
102
backend/tests/test_client_upload_companion_collisions.py
Normal file
102
backend/tests/test_client_upload_companion_collisions.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Converted uploads must preserve Markdown from earlier upload calls."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from openpyxl import Workbook
|
||||
from pptx import Presentation
|
||||
|
||||
from deerflow.client import DeerFlowClient
|
||||
from deerflow.uploads.companions import resolve_companion
|
||||
|
||||
|
||||
def _document(path: Path, content: str) -> None:
|
||||
if path.suffix == ".md":
|
||||
path.write_text(content, encoding="utf-8")
|
||||
elif path.suffix == ".xlsx":
|
||||
book = Workbook()
|
||||
book.active["A1"] = content
|
||||
book.save(path)
|
||||
book.close()
|
||||
else:
|
||||
slides = Presentation()
|
||||
slide = slides.slides.add_slide(slides.slide_layouts[1])
|
||||
slide.shapes.title.text = content
|
||||
slides.save(path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def upload_case(tmp_path, monkeypatch):
|
||||
import deerflow.client as client_module
|
||||
|
||||
uploads = tmp_path / "thread/user-data/uploads"
|
||||
uploads.mkdir(parents=True)
|
||||
monkeypatch.setattr(client_module, "ensure_uploads_dir", lambda *args, **kwargs: uploads)
|
||||
return DeerFlowClient.__new__(DeerFlowClient), uploads
|
||||
|
||||
|
||||
@pytest.mark.parametrize("in_loop", [False, True])
|
||||
@pytest.mark.parametrize("first_ext,second_ext", [(".md", ".xlsx"), (".md", ".pptx"), (".xlsx", ".pptx"), (".pptx", ".xlsx")])
|
||||
def test_separate_uploads_preserve_existing_markdown(upload_case, tmp_path, in_loop, first_ext, second_ext):
|
||||
client, uploads = upload_case
|
||||
first = tmp_path / f"report{first_ext}"
|
||||
second = tmp_path / f"report{second_ext}"
|
||||
_document(first, "KEEP FIRST DOCUMENT")
|
||||
_document(second, "SECOND DOCUMENT")
|
||||
|
||||
def upload(source):
|
||||
if not in_loop:
|
||||
return client.upload_files("companion-thread", [source])
|
||||
|
||||
# Run the synchronous upload inside an active event loop to exercise conversion_pool.
|
||||
async def run():
|
||||
return client.upload_files("companion-thread", [source])
|
||||
|
||||
return asyncio.run(run())
|
||||
|
||||
first_response = upload(first)
|
||||
original_markdown = uploads / "report.md"
|
||||
original_bytes = original_markdown.read_bytes()
|
||||
second_response = upload(second)
|
||||
|
||||
assert first_response["success"] and second_response["success"]
|
||||
assert original_markdown.read_bytes() == original_bytes
|
||||
assert "KEEP FIRST DOCUMENT" in original_markdown.read_text(encoding="utf-8")
|
||||
info = second_response["files"][0]
|
||||
assert info["filename"] == second.name
|
||||
assert info["markdown_file"] == "report_1.md"
|
||||
companion = uploads / info["markdown_file"]
|
||||
assert "SECOND DOCUMENT" in companion.read_text(encoding="utf-8")
|
||||
assert resolve_companion(uploads / second.name) == companion
|
||||
assert Path(info["markdown_path"]) == companion
|
||||
assert info["markdown_virtual_path"].endswith("/report_1.md")
|
||||
assert info["markdown_artifact_url"].endswith("/report_1.md")
|
||||
assert (uploads / first.name).read_bytes() == first.read_bytes()
|
||||
assert (uploads / second.name).read_bytes() == second.read_bytes()
|
||||
if first_ext != ".md":
|
||||
assert resolve_companion(uploads / first.name) == original_markdown
|
||||
|
||||
|
||||
def test_reupload_replaces_original_but_preserves_occupied_companion_names(upload_case, tmp_path):
|
||||
client, uploads = upload_case
|
||||
source = tmp_path / "report.xlsx"
|
||||
_document(source, "FIRST DOCUMENT")
|
||||
client.upload_files("companion-thread", [source])
|
||||
first_markdown = (uploads / "report.md").read_bytes()
|
||||
occupied = uploads / "report_1.md"
|
||||
occupied.write_text("INDEPENDENT NOTES", encoding="utf-8")
|
||||
|
||||
_document(source, "REPLACEMENT DOCUMENT")
|
||||
response = client.upload_files("companion-thread", [source])
|
||||
|
||||
assert response["success"]
|
||||
info = response["files"][0]
|
||||
assert info["filename"] == "report.xlsx"
|
||||
assert (uploads / source.name).read_bytes() == source.read_bytes()
|
||||
assert info["markdown_file"] == "report_2.md"
|
||||
assert (uploads / "report.md").read_bytes() == first_markdown
|
||||
assert occupied.read_text(encoding="utf-8") == "INDEPENDENT NOTES"
|
||||
companion = uploads / "report_2.md"
|
||||
assert "REPLACEMENT DOCUMENT" in companion.read_text(encoding="utf-8")
|
||||
assert resolve_companion(uploads / source.name) == companion
|
||||
Loading…
x
Reference in New Issue
Block a user