+
{typeLabel}
diff --git a/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts b/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts
index 6d2bf725..72f05b81 100644
--- a/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts
+++ b/frontend-web/src/roundtable-planning/hooks/useStep3Report.ts
@@ -17,8 +17,10 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useArtifacts } from "@/components/workspace/artifacts";
+import { apiFetch } from "@/core/api";
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
import {
+ buildArtifactDownloadUrl,
initReportThread,
listSessionArtifacts,
streamMultiAgent,
@@ -139,6 +141,11 @@ function buildReportPrompt(opts: {
"文字为主图表点睛(全篇穿插 3–5 个有信息量的图、每图配一两句解读)、浅红主题(主色 #c25555);" +
"写完文件后只用一两句话总结,不要把 HTML 源码贴进对话。",
);
+ // 语言 + 编码硬约束:偶发整篇输出英文 / 中文乱码的两个根因都在这里堵住。
+ parts.push(
+ "硬性要求:① 全程使用简体中文输出——HTML 正文、标题、图表标签与图例、按钮、结论摘要等一切文字都必须是中文,禁止输出英文(专有名词可中英并列);" +
+ '② HTML 必须是完整文档,并在 最前面声明 ,确保中文不乱码。',
+ );
return parts.join("\n");
}
@@ -543,6 +550,51 @@ export function useStep3Report(
[setArtifacts, selectArtifact, setArtifactsOpen],
);
+ // 收尾对账:以沙箱真实文件为准回灌 stub,拿到完整 HTML。
+ // 智能体可能分多次写文件 —— 尤其是 write_file 带 `append=true` 续写后半段(截图里
+ // 「Append remaining HTML content…」那一步)。后端 append 会把后半段拼到磁盘文件已有
+ // 内容后面,磁盘文件是完整的;但前端 stub 按 (name,path) 去重,第二次 write_file 会用
+ // 「只含后半段」的 content 覆盖掉前半段(防抹空守卫只挡空内容、挡不住非空的后半段)。
+ // 这里流式结束后从 outputs 读真实文件回灌 stub,让预览与存档都拿到完整 HTML。
+ // 返回最终用于存草稿的 HTML(拉取失败 / 真实文件更短时退回 stub 内容,不打断收尾)。
+ const reconcileStubFromRealFile = useCallback(
+ async (threadId: string, signal?: AbortSignal): Promise => {
+ const writeTc = stubRef.current[threadId]?.[0]?.tool_calls.find(
+ (tc) => tc.name === "write_file",
+ );
+ const stubPath =
+ typeof (writeTc?.args as { path?: unknown })?.path === "string"
+ ? ((writeTc!.args as { path?: string }).path as string)
+ : "";
+ const stubContent =
+ typeof (writeTc?.args as { content?: unknown })?.content === "string"
+ ? ((writeTc!.args as { content?: string }).content as string)
+ : "";
+ if (!stubPath) return stubContent;
+ try {
+ const list = await listSessionArtifacts(threadId, { signal });
+ const htmlFile =
+ list.files.find((f) => /\.html?$/i.test(f.name)) ?? null;
+ if (!htmlFile) return stubContent;
+ const res = await apiFetch(
+ buildArtifactDownloadUrl(threadId, htmlFile.path),
+ { signal },
+ );
+ if (!res.ok) return stubContent;
+ const real = await res.text();
+ // 只在真实文件更长(说明 stub 被后半段截短了)时替换,避免偶发空/短响应回退。
+ if (real.trim() && real.length > stubContent.length) {
+ upsertStub(threadId, "write_file", { path: stubPath, content: real });
+ return real;
+ }
+ } catch {
+ /* 拉取真实文件失败就退回 stub 内容,不打断主流程 */
+ }
+ return stubContent;
+ },
+ [upsertStub],
+ );
+
const abort = useCallback(() => {
aborterRef.current?.abort();
aborterRef.current = null;
@@ -604,17 +656,16 @@ export function useStep3Report(
const hasStubFile = (stubRef.current[threadId]?.[0]?.tool_calls.length ?? 0) > 0;
if (!hasStubFile) await openRealArtifactFallback(threadId);
+ // 以沙箱真实文件回灌 stub(修复 append 续写只剩后半段的问题),并拿到完整 HTML。
+ const html = hasStubFile
+ ? await reconcileStubFromRealFile(threadId, aborter.signal)
+ : "";
+ if (aborter.signal.aborted) return;
+
finalizeDialogue(bubbleId, res.content, "绘制完成");
setPhase("done");
- // 绘制成功 → 把完整 HTML 等抛给上层存进草稿(step3)。从 stub 取最新 write_file 内容。
- const writeTc = stubRef.current[threadId]?.[0]?.tool_calls.find(
- (tc) => tc.name === "write_file",
- );
- const html =
- typeof (writeTc?.args as { content?: unknown })?.content === "string"
- ? ((writeTc!.args as { content?: string }).content as string)
- : "";
+ // 绘制成功 → 把完整 HTML 等抛给上层存进草稿(step3)。
if (html.trim()) {
onReportSavedRef.current?.({
html,
@@ -645,6 +696,7 @@ export function useStep3Report(
handleArtifactToolPhase,
handleUpstreamFrame,
openRealArtifactFallback,
+ reconcileStubFromRealFile,
]);
const start = useCallback(() => {
diff --git a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-report/SOUL.md b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-report/SOUL.md
index 933bc760..30756662 100644
--- a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-report/SOUL.md
+++ b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed_assets/roundtable-report/SOUL.md
@@ -30,7 +30,7 @@
| # | 章节 | 内容(文字) | 配图建议 |
|---|------|------|----------|
-| ① | 报告头 | 标题 + 生成时间占位 + 「AI 自动生成」标签 | — |
+| ① | 报告头 | 标题 + 生成时间占位 | — |
| ② | 一句话结论 + 关键指标卡 | 用一句话点明最终方案/结论 | 4–6 个 KPI 数字卡(共识度、参与席位数、识别风险数、里程碑数、预估周期、预算规模等) |
| ③ | 任务背景与目标 | 2–4 句交代要解决什么、目标与关键约束 | 可选:目标/约束要点列表 |
| ④ | 各席位观点与交付 | **每个席位一段提炼文字**(这位专家关注什么、给出了什么关键结论/建议),用小标题+正文,让读者读懂每方的贡献 | 关键席位视情况配小图(如情报席位的对比、环境席位的承载评估) |
@@ -73,4 +73,5 @@
- ❌ 写 Python / shell / 任何脚本去生成 HTML,或用 bash 跑脚本产出文件(必须自己把 HTML 文本直接写进 `write_file`)
- ❌ 把 HTML 拆成多文件或分多次拼接、或写完后再去读取/校验文件
- ❌ 用 `str_replace` 局部修改 HTML(返工请整文件 `write_file` 重写)
+- ❌ 在报告头或任何位置添加「AI 自动生成」「AI generated」之类的标签、徽章或水印
- ❌ 编造与原料明显矛盾的数据