diff --git a/CHANGELOG.md b/CHANGELOG.md index 89934e1439..3aee3765e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2404,6 +2404,14 @@ This release closes that milestone with **772 merged pull requests**. ### Fixed +- **uploads:** A malformed `files[*].size` in a run's message metadata no + longer fails the whole run. `UploadsMiddleware` validated every other field + of a client-supplied file entry fail-soft but passed `size` straight to + `int()`, so a value such as `"abc"` or a list raised out of `before_agent` + before the model was called — and again on every edit or regenerate of that + message, since the entry is carried over verbatim. The size only feeds the + human-readable line in ``; unusable values now fall back to + `0`, the same as a missing size, while numeric strings keep working. ([#5855]) - **release:** Bumping the version no longer leaves `backend/uv.lock` behind. `scripts/bump_version.sh` rewrote `backend/pyproject.toml`, `frontend/package.json` and the Helm chart, but the lockfile records the root package's own version too @@ -6077,5 +6085,6 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5844]: https://github.com/bytedance/deer-flow/pull/5844 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 +[#5855]: https://github.com/bytedance/deer-flow/pull/5855 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index 96eab4188d..fbc0cbeeca 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -2120,6 +2120,11 @@ ### 修复 +- **上传:** 运行消息元数据中格式错误的 `files[*].size` 不再导致整个运行失败。 + `UploadsMiddleware` 对客户端提供的文件条目的其他字段都做了容错校验,唯独把 `size` 直接交给 + `int()`,因此 `"abc"` 或列表这样的值会在调用模型之前从 `before_agent` 抛出——而且由于该条目 + 会被原样带入,之后每次编辑或重新生成这条消息都会再次失败。该字段只用于 `` + 里的可读大小;现在无法使用的值会回退为 `0`,与缺失时一致,数字字符串仍然有效。([#5855]) - **发布:** 版本升级不再把 `backend/uv.lock` 落下。`scripts/bump_version.sh` 会改写 `backend/pyproject.toml`、`frontend/package.json` 与 Helm chart,但 lockfile 同样记录了 根包自身的版本(uv 保留其 PEP 440 形式,因此 `2.1.0-rc0` 存为 `2.1.0rc0`),于是文档给出的 @@ -5175,4 +5180,5 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5844]: https://github.com/bytedance/deer-flow/pull/5844 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 +[#5855]: https://github.com/bytedance/deer-flow/pull/5855 [#5859]: https://github.com/bytedance/deer-flow/pull/5859 diff --git a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md index 3826b948e7..df52bd7a20 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md +++ b/backend/packages/harness/deerflow/agents/middlewares/AGENTS.md @@ -67,7 +67,7 @@ strict providers reject. 4. **PiiRedactionMiddleware** - *(optional, `pii_redaction.enabled`, default off, #3190)* Rewrites PII in genuine user messages (`wrap_model_call`) and remote-content tool results (`wrap_tool_call`, the tool-result allowlist) to irreversible value-derived placeholders. Deterministic regex detectors only. Innermost Layer-1 wrapper; compaction input, summaries, title input and queued memory payloads are redacted via `redact_text`. 5. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves identity via `resolve_runtime_user_id(runtime)`, including Gateway runtime context and standalone LangGraph Server auth, then falls back to the request ContextVar / `"default"` -6. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation (lead agent only); upload existence checks use the same runtime-resolved user bucket as thread-data creation +6. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation (lead agent only); upload existence checks use the same runtime-resolved user bucket as thread-data creation; a malformed client-supplied `files[*].size` is coerced to `0` (like a missing one), never raised 7. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state. The lead runtime normally owns the thread's physical Agent-skill projection; delegated subagents and the prompt-only bootstrap agent are non-owners, so diff --git a/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py index 0cda71a482..f9ac936354 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py @@ -160,6 +160,25 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]): return "\n".join(lines) + @staticmethod + def _coerce_file_size(value: object) -> int: + """Best-effort byte count from a client-supplied ``files[*].size``. + + The value only feeds the human-readable size in ````, + and every other field in ``_files_from_kwargs`` is validated fail-soft, + so an unusable size must never abort the run: anything that is not a + non-negative finite number (``"abc"``, a list, a bool, ``-5``, ``inf``) + falls back to ``0``, the same value a missing size gets. Numeric + strings such as ``"2048"`` keep working. + """ + if value is None or isinstance(value, bool): + return 0 + try: + size = int(value) + except (TypeError, ValueError, OverflowError): + return 0 + return size if size >= 0 else 0 + def _files_from_kwargs(self, message: HumanMessage, uploads_dir: Path | None = None) -> list[dict] | None: """Extract file info from message additional_kwargs.files. @@ -191,7 +210,7 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]): files.append( { "filename": filename, - "size": int(f.get("size") or 0), + "size": self._coerce_file_size(f.get("size")), "path": f"/mnt/user-data/uploads/{filename}", "extension": Path(filename).suffix, } diff --git a/backend/tests/test_uploads_middleware_core_logic.py b/backend/tests/test_uploads_middleware_core_logic.py index 832a176800..6eb0a4c7e8 100644 --- a/backend/tests/test_uploads_middleware_core_logic.py +++ b/backend/tests/test_uploads_middleware_core_logic.py @@ -7,10 +7,12 @@ Covers: additional_kwargs, historical files from uploads dir, edge-cases) """ +import math import re from pathlib import Path from unittest.mock import MagicMock +import pytest from langchain_core.messages import AIMessage, HumanMessage from deerflow.agents.middlewares.uploads_middleware import UploadsMiddleware @@ -159,6 +161,30 @@ class TestFilesFromKwargs: assert result is not None assert result[0]["size"] == 0 + @pytest.mark.parametrize( + "size", + ["abc", "12.5", "", [1], {"n": 1}, None, True, -5, math.inf, math.nan], + ids=["word", "decimal-string", "empty-string", "list", "dict", "none", "bool", "negative", "inf", "nan"], + ) + def test_unusable_size_falls_back_to_zero_instead_of_raising(self, tmp_path, size): + """``files[*].size`` is client-supplied display metadata; every other field in this loop is validated fail-soft.""" + mw = _middleware(tmp_path) + msg = _human("hi", files=[{"filename": "f.txt", "size": size, "path": "/mnt/user-data/uploads/f.txt"}]) + result = mw._files_from_kwargs(msg) + assert result is not None + assert result[0]["filename"] == "f.txt" + assert result[0]["size"] == 0 + assert type(result[0]["size"]) is int + + @pytest.mark.parametrize("size, expected", [(2048, 2048), ("2048", 2048), (2048.0, 2048), (" 2048 ", 2048)], ids=["int", "string", "float", "padded-string"]) + def test_usable_size_is_kept(self, tmp_path, size, expected): + mw = _middleware(tmp_path) + msg = _human("hi", files=[{"filename": "f.txt", "size": size, "path": "/mnt/user-data/uploads/f.txt"}]) + result = mw._files_from_kwargs(msg) + assert result is not None + assert result[0]["size"] == expected + assert type(result[0]["size"]) is int + def test_skips_upload_staging_filenames(self, tmp_path): mw = _middleware(tmp_path) msg = _human("hi", files=[{"filename": ".upload-active.part", "size": 5, "path": "/mnt/user-data/uploads/.upload-active.part"}]) @@ -249,6 +275,22 @@ class TestBeforeAgent: result = mw.before_agent(state, _runtime()) assert result == {"uploaded_files": []} + def test_unusable_size_does_not_fail_the_run(self, tmp_path): + """A malformed size on an existing upload must still inject the file, not abort before the model is called.""" + mw = _middleware(tmp_path) + uploads_dir = _uploads_dir(tmp_path) + (uploads_dir / "present.txt").write_text("hello", encoding="utf-8") + msg = _human("hi", files=[{"filename": "present.txt", "size": "abc", "path": "/mnt/user-data/uploads/present.txt"}]) + state = self._state(msg) + + result = mw.before_agent(state, _runtime()) + + assert result is not None + assert [f["filename"] for f in result["uploaded_files"]] == ["present.txt"] + assert result["uploaded_files"][0]["size"] == 0 + block = _current_uploads_block(result["messages"][0].content) + assert "present.txt" in block + def test_clears_uploaded_files_when_all_files_missing_from_disk(self, tmp_path): mw = _middleware(tmp_path) _uploads_dir(tmp_path) # directory exists but is empty