= ({
加载失败:{error}
- ) : content === null ? null : renderAsMarkdown ? (
+ ) : content === null ? null : renderAsMermaid ? (
+ mermaidSvg ? (
+
+ ) : (
+
+
+ 正在生成图表预览
+
+ )
+ ) : renderAsMarkdown ? (
{content}
diff --git a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx
index 117e0667..a4c30959 100644
--- a/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx
+++ b/frontend-web/src/roundtable-planning/components/Step3ReportMode.tsx
@@ -648,7 +648,7 @@ function shouldPreviewInModal(file: { name: string; mimeType: string | null }):
const mime = file.mimeType ?? "";
if (mime.startsWith("text/")) return true;
if (["application/json"].includes(mime)) return true;
- return /\.(md|markdown|txt|json|csv|log|yml|yaml|xml|html?)$/i.test(file.name);
+ return /\.(md|markdown|mmd|txt|json|csv|log|yml|yaml|xml|html?)$/i.test(file.name);
}
function formatBytes(n: number): string {
diff --git a/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py b/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py
index e7530b64..c097d897 100644
--- a/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py
+++ b/offline-backend-20260512/backend/app/gateway/roundtable_job_executor.py
@@ -288,6 +288,8 @@ class RoundtableJobExecutor:
idx = next((i for i, r in enumerate(runs) if r.get("id") == run_id), -1)
created = runs[idx].get("createdAt") if idx >= 0 else datetime.now(UTC).isoformat()
prev_step3 = runs[idx].get("step3") if idx >= 0 else None
+ # 本次有效 step3:新报告优先,否则保留上次(running 中间态 step3=None 不抹掉已存的)。
+ effective_step3 = step3 if step3 is not None else prev_step3
run = {
"id": run_id,
"title": "后台研讨",
@@ -308,17 +310,21 @@ class RoundtableJobExecutor:
"seatStubMessages": _seat_stub_messages(dialogues),
"orchestrationMode": params.mode,
"chain": params.chain,
- "step3": step3 if step3 is not None else prev_step3,
+ "step3": effective_step3,
"jobId": params.job_id,
"jobStatus": status,
}
runs = [run if i == idx else r for i, r in enumerate(runs)] if idx >= 0 else [*runs, run]
step2["runs"] = runs
step2["activeRunId"] = run_id
+ # 顶层 draft.step3 也写一份(同 Step2 把对话写进 run 的同一次落库一并带上)。
+ # 前端 hydrateStep3 用**顶层** draft.step3 重建报告气泡(结果绘制的聊天记录 + HTML 预览);
+ # 只存 run 内 step3 时,加载会被 hydrateStep3(draft.step3=None) 覆盖成空 → 报告聊天记录丢失。
+ update_kwargs: dict[str, Any] = {"step2": step2, "furthest_step": 3 if status == "done" else 2}
+ if effective_step3 is not None:
+ update_kwargs["step3"] = effective_step3
try:
- await self._draft_store.update_draft(
- params.draft_id, params.user_id, step2=step2, furthest_step=3 if status == "done" else 2
- )
+ await self._draft_store.update_draft(params.draft_id, params.user_id, **update_kwargs)
except Exception: # noqa: BLE001 — 写草稿失败不影响作业本身
logger.warning("write run to draft %s failed", params.draft_id, exc_info=True)
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py
index 17aaa51f..1dd314ac 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/roundtable_orchestrator/engine.py
@@ -138,7 +138,11 @@ async def run_orchestration(
_SEAT_NO_ASK = (
"注意:这是自动化圆桌研讨,没有用户在线应答——**请勿向用户提问或等待确认**,"
- "直接基于已知信息做出合理假设并产出完整的结构化交付(Markdown)。"
+ "直接基于已知信息做出合理假设,产出完整的结构化交付。\n"
+ "**交付方式(与前端手动研讨一致)**:请用 write_file 工具把你这一轮的完整交付整理成一份 "
+ "Markdown 文件,写入 /mnt/user-data/outputs/ 下(文件名体现你的职责,如 `情报收集.md`、"
+ "`风险评估.md`);同时在回复正文里给出这份交付的内容本身(供总控汇总),"
+ "不要只回复『已写入文件』这类空话。"
)
diff --git a/offline-backend-20260512/backend/scripts/inspect_latest_roundtable_run.py b/offline-backend-20260512/backend/scripts/inspect_latest_roundtable_run.py
new file mode 100644
index 00000000..a7392196
--- /dev/null
+++ b/offline-backend-20260512/backend/scripts/inspect_latest_roundtable_run.py
@@ -0,0 +1,114 @@
+"""一次性巡检:把最新圆桌草稿里「后台作业 run」的关键字段 dump 到文件,
+用于验证维度3 存储 + 报告 HTML + 席位 md 文件是否都对齐了。
+
+用法(在 backend 目录):
+ PYTHONPATH=. uv run python scripts/inspect_latest_roundtable_run.py
+
+输出写到 scripts/_roundtable_run_report.txt(UTF-8),直接打开看,避免 PowerShell GBK 乱码。
+"""
+
+from __future__ import annotations
+
+import json
+import sqlite3
+from pathlib import Path
+
+DB = Path(".deer-flow/data/deerflow.db")
+OUT = Path("scripts/_roundtable_run_report.txt")
+
+
+def _loads(v):
+ if isinstance(v, (bytes, bytearray)):
+ v = v.decode("utf-8", "replace")
+ if isinstance(v, str):
+ try:
+ return json.loads(v)
+ except Exception:
+ return v
+ return v
+
+
+def main() -> None:
+ con = sqlite3.connect(str(DB))
+ con.row_factory = sqlite3.Row
+ lines: list[str] = []
+
+ # 最新一条有 step2.runs 的草稿
+ rows = con.execute(
+ "SELECT id, step2, step3 FROM roundtable_drafts ORDER BY updated_at DESC LIMIT 5"
+ ).fetchall()
+
+ draft = None
+ top_step3 = None
+ for r in rows:
+ s2 = _loads(r["step2"]) or {}
+ if isinstance(s2, dict) and s2.get("runs"):
+ draft = (r["id"], s2)
+ top_step3 = _loads(r["step3"])
+ break
+
+ if not draft:
+ lines.append("没找到带 step2.runs 的草稿。")
+ OUT.write_text("\n".join(lines), encoding="utf-8")
+ print(f"written: {OUT}")
+ return
+
+ draft_id, s2 = draft
+ runs = s2.get("runs") or []
+ job_run = next((x for x in runs if str(x.get("id", "")).startswith("job-")), runs[-1])
+
+ lines.append(f"draft_id = {draft_id}")
+ lines.append(f"run.id = {job_run.get('id')} jobStatus = {job_run.get('jobStatus')} hasConsensus = {job_run.get('hasConsensus')}")
+ lines.append("")
+
+ # ── 维度3:threadIds ──
+ lines.append("【threadIds】(应为真实 map,不是 null)")
+ lines.append(json.dumps(job_run.get("threadIds"), ensure_ascii=False, indent=2))
+ lines.append("")
+
+ # ── 维度3:各对话的 steps / agentThreadId / time ──
+ lines.append("【step2RoundtableDialogues 摘要】")
+ for d in job_run.get("step2RoundtableDialogues") or []:
+ steps = d.get("steps") or []
+ step_keys = [s.get("key") for s in steps]
+ lines.append(
+ f"- [{d.get('sender')}] time={d.get('time')!r} thread={d.get('agentThreadId')!r} "
+ f"steps={step_keys} content_len={len(d.get('content') or '')}"
+ )
+ lines.append("")
+
+ # ── 席位 md 文件:seatStubMessages 里有没有 write_file ──
+ lines.append("【seatStubMessages】(席位写的文件 → 文件卡片数据源)")
+ stubs = job_run.get("seatStubMessages") or {}
+ if not stubs:
+ lines.append(" (空 —— 席位没有写文件 / 没采集到)")
+ for tid, msgs in stubs.items():
+ for m in msgs:
+ for tc in m.get("tool_calls") or []:
+ args = tc.get("args") or {}
+ lines.append(f" thread={tid} {tc.get('name')} path={args.get('path')!r} content_len={len(str(args.get('content') or ''))}")
+ lines.append("")
+
+ # ── 报告 HTML(P0)──
+ lines.append("【step3 报告】")
+ step3 = job_run.get("step3") or {}
+ html = step3.get("html") or ""
+ lines.append(f" html_len = {len(html)}")
+ lines.append(f" 含