Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,12 @@ Multi-file upload with automatic document conversion:
- Gateway HTTP upload/list/delete handlers offload filesystem work through `deerflow.utils.file_io.run_file_io`, a dedicated ContextVar-preserving file IO executor. Non-mounted sandbox uploads acquire sandboxes with `SandboxProvider.acquire_async()` and offload `read_bytes()` plus `sandbox.update_file()` together.
- Mounted upload paths skip both sandbox acquisition and per-file synchronization. For AIO remote/provisioner deployments this requires an explicit, accurate `sandbox.thread_data_mounts: true`; omission preserves backend auto-detection.
- Agent receives uploaded file list via `UploadsMiddleware`
- Current-turn web upload metadata preserves the nullable `markdown_file`
provenance returned by the upload API. `UploadsMiddleware` validates an
explicit companion basename and reads that exact file; explicit null or
invalid metadata disables sibling guessing, while an absent key retains the
legacy `<stem>.md` fallback for old messages. Raw companion metadata is never
rendered into `<current_uploads>`.

See [docs/FILE_UPLOAD.md](docs/FILE_UPLOAD.md) for details.

Expand Down
12 changes: 11 additions & 1 deletion backend/docs/FILE_UPLOAD.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,17 @@ DELETE /api/threads/{thread_id}/uploads/{filename}
### 当前消息中的文件上下文

发送消息时,前端会把该消息附带的上传文件元数据放入
`HumanMessage.additional_kwargs.files`。`UploadsMiddleware` 只把当前消息中的文件
`HumanMessage.additional_kwargs.files`。

Web 上传会把上传响应中的 `markdown_file` 一并写入每个文件的结构化元数据,
使 `UploadsMiddleware` 能按显式的“源文件 → 转换 Markdown”关系提取大纲;
同名源文件发生冲突重命名时,例如 `a.pdf → a_1.md`,不会再按
`a.pdf → a.md` 猜测。新消息中的 `markdown_file: null` 表示本次上传明确
没有转换产物,因此不回退到同名 Markdown。为兼容旧客户端和历史消息,
只有在该字段完全缺失时才沿用 `<stem>.md` 查找。非法、越界或已失效的
companion 元数据会被忽略,原始上传文件仍保留在 Agent 上下文中。

`UploadsMiddleware` 只把当前消息中的文件
注入 Agent 上下文,格式如下:

```xml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from deerflow.config.paths import Paths, get_paths
from deerflow.runtime.user_context import resolve_runtime_user_id
from deerflow.uploads.manager import is_upload_staging_file
from deerflow.utils.file_outline import extract_outline_for_file
from deerflow.utils.file_outline import extract_outline_for_file, extract_outline_from_uploaded_markdown
from deerflow.utils.messages import ORIGINAL_USER_CONTENT_KEY, message_content_to_text

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -160,6 +160,50 @@ def _create_files_message(

return "\n".join(lines)

@staticmethod
def _normalize_markdown_companion(
file_metadata: dict,
uploads_dir: Path | None,
source_filename: str,
) -> tuple[bool, str | None]:
"""Return explicit-key presence and a safe Markdown companion basename."""
if "markdown_file" not in file_metadata:
return False, None

raw = file_metadata.get("markdown_file")
if raw is None:
return True, None

reason: str | None = None
if not isinstance(raw, str) or not raw:
reason = "value must be a non-empty string or null"
elif "/" in raw or "\\" in raw or Path(raw).name != raw:
reason = "value must be a basename"
elif ":" in raw:
reason = "value must not contain alternate stream syntax"
elif is_upload_staging_file(raw):
reason = "staging files are not valid companions"
elif Path(raw).suffix.lower() != ".md":
reason = "value must have a .md suffix"
elif uploads_dir is not None:
# Preliminary state normalization only. before_agent performs the
# authoritative no-follow validation on the handle it parses.
try:
candidate = uploads_dir / raw
if candidate.is_symlink() or not candidate.is_file():
reason = "file is missing or not a regular file"
except (OSError, ValueError):
reason = "file cannot be inspected safely"

if reason is not None:
logger.warning(
"Ignoring Markdown companion metadata for upload %s: %s",
source_filename,
reason,
)
return True, None
return True, raw

def _files_from_kwargs(self, message: HumanMessage, uploads_dir: Path | None = None) -> list[dict] | None:
"""Extract file info from message additional_kwargs.files.

Expand Down Expand Up @@ -188,14 +232,20 @@ def _files_from_kwargs(self, message: HumanMessage, uploads_dir: Path | None = N
continue
if uploads_dir is not None and not (uploads_dir / filename).is_file():
continue
files.append(
{
"filename": filename,
"size": int(f.get("size") or 0),
"path": f"/mnt/user-data/uploads/{filename}",
"extension": Path(filename).suffix,
}
file_info = {
"filename": filename,
"size": int(f.get("size") or 0),
"path": f"/mnt/user-data/uploads/{filename}",
"extension": Path(filename).suffix,
}
has_explicit_companion, markdown_file = self._normalize_markdown_companion(
f,
uploads_dir,
filename,
)
if has_explicit_companion:
file_info["markdown_file"] = markdown_file
files.append(file_info)
return files if files else None

@override
Expand Down Expand Up @@ -247,7 +297,12 @@ def before_agent(self, state: UploadsMiddlewareState, runtime: Runtime) -> dict
if uploads_dir:
for file in context_files:
phys_path = uploads_dir / file["filename"]
outline, preview = extract_outline_for_file(phys_path)
if "markdown_file" not in file:
outline, preview = extract_outline_for_file(phys_path)
elif file["markdown_file"] is None:
outline, preview = [], []
else:
outline, preview = extract_outline_from_uploaded_markdown(uploads_dir, file["markdown_file"])
file["outline"] = outline
file["outline_preview"] = preview

Expand Down
Loading