正在加载文件内容...
diff --git a/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx b/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx
index fe01f2e5..31bc8438 100644
--- a/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx
+++ b/frontend-web/src/pages/ScheduledTaskRunDetailPage.tsx
@@ -270,7 +270,16 @@ export default function ScheduledTaskRunDetailPage() {
}
};
- const resultFiles = useMemo(() => result?.files ?? [], [result]);
+ // The generated HTML document is already shown in the dedicated "HTML 预览"
+ // tab, so it is excluded from the "结果文件" list — that list surfaces the
+ // Markdown (and any other non-HTML artifact) only.
+ const resultFiles = useMemo(
+ () =>
+ (result?.files ?? []).filter(
+ (f) => !(f.kind === "html" || (f.name ?? "").toLowerCase().endsWith(".html")),
+ ),
+ [result],
+ );
const fileCount = resultFiles.length;
const resultMetadata = useMemo(() => {
if (!result) return "";
@@ -295,18 +304,28 @@ export default function ScheduledTaskRunDetailPage() {
}
setFilesLoading(true);
try {
- const loaded = await Promise.all(
+ // Load each file independently. A single failing file (e.g. an archive
+ // that 404s) must NOT blank the whole panel — otherwise the tab shows
+ // "结果文件(2)" while the body says "没有生成文件". Render every file
+ // that loads and skip the ones that don't.
+ const settled = await Promise.allSettled(
resultFiles.map((f) => {
const index = result.files.indexOf(f);
return getScheduledTaskRunFileContent(run.id, index);
}),
);
if (!cancelled) {
+ const loaded = settled
+ .filter(
+ (r): r is PromiseFulfilledResult
=>
+ r.status === "fulfilled",
+ )
+ .map((r) => r.value);
setFileContents(loaded);
- }
- } catch (error) {
- if (!cancelled) {
- toast.error(error instanceof Error ? error.message : "加载文件内容失败");
+ const failed = settled.length - loaded.length;
+ if (failed > 0 && loaded.length === 0) {
+ toast.error("结果文件已不可用(可能已被清理)");
+ }
}
} finally {
if (!cancelled) {
@@ -584,7 +603,9 @@ export default function ScheduledTaskRunDetailPage() {
)}
{!filesLoading && fileContents.length === 0 && (
- 本次执行没有生成文件。
+ {fileCount > 0
+ ? `本次执行生成了 ${fileCount} 个文件,但当前无法读取(文件可能已被清理或移动)。可尝试「下载全部」。`
+ : "本次执行没有生成文件。"}
)}
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py
index 43c93b69..3358984c 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/uploads_middleware.py
@@ -243,6 +243,17 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
# Get newly uploaded files from the current message's additional_kwargs.files
new_files = self._files_from_kwargs(last_message, uploads_dir) or []
+ # Only inject when the *current* message actually carries new uploads.
+ # Turns without a fresh upload must not re-inject previously uploaded
+ # files: the original upload turn already injected (and persisted) their
+ # block, and the files remain physically available for
+ # read_file/grep/view_image. Re-injecting on every later message made the
+ # agent re-reference old images/docs, and — since the frontend parses
+ # files out of the content when additional_kwargs.files
+ # is empty — re-rendered the old attachment on each new question.
+ if not new_files:
+ return None
+
# Collect historical files from the uploads directory (all except the new ones)
new_filenames = {f["filename"] for f in new_files}
historical_files: list[dict] = []
@@ -270,9 +281,6 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
file["outline"] = outline
file["outline_preview"] = preview
- if not new_files and not historical_files:
- return None
-
logger.debug(f"New files: {[f['filename'] for f in new_files]}, historical: {[f['filename'] for f in historical_files]}")
# Create files message and prepend to the last human message content
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
index deeb3e63..93adcb1e 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
@@ -1007,11 +1007,11 @@ class ScheduledTaskService:
if negative_prompt:
parts.append(f"限制条件(最高优先级,必须严格遵守):{negative_prompt}")
gather_prompt = "\n".join(parts)
- # When the Markdown switch is also on, ask this same gather pass to save a
- # .md file (the length gate is enforced in code, not surfaced here) so the
- # task produces both deliverables in one agent run.
- if also_markdown:
- gather_prompt += _MARKDOWN_INSTRUCTION
+ # When the Markdown switch is also on, this same gather pass must save a
+ # .md file. We do NOT append _MARKDOWN_INSTRUCTION here — `_start_agent_run`
+ # already appends it exactly once when `require_markdown` is present in the
+ # execution context (kept below). Appending it here too made the prompt
+ # carry the 【输出要求】 block twice when both HTML and Markdown were on.
gather_task = {**task, "prompt": gather_prompt}
gather_ctx = dict(task.get("execution_context") or {})
# Strip the HTML-specific flags so the gather pass is a plain agent run.