暂无结果报告
+这次会商还没有生成第三步「结果绘制」的方案报告。
+本次会商暂无子智能体交付。
+ ); + } + return ( +` with NO authentication — anyone with
- * the link can open it. It renders the Step 3「结果绘制」report (a self-contained
- * HTML document) inside a sandboxed iframe with no `allow-scripts`, mirroring the
- * scheduled-task HTML-page preview, so a shared report can never execute code in
- * the app origin.
+ * the link can open it. Two views, driven by the `report` prop / route:
+ *
+ * - landing (`#/roundtable-share/`): a read-only result page mirroring the
+ * in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」
+ * button that opens the full report in a NEW TAB (when one was generated).
+ * - report (`#/roundtable-share//report`): the Step 3 report (a
+ * self-contained HTML document) rendered full-screen inside a sandboxed
+ * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute
+ * code in the app origin.
*/
-export default function PublicRoundtableSharePage() {
+export default function PublicRoundtableSharePage({
+ report = false,
+}: {
+ report?: boolean;
+}) {
const { shareCode } = useParams<{ shareCode: string }>();
const { data, isLoading, isError } = useQuery({
@@ -46,37 +125,113 @@ export default function PublicRoundtableSharePage() {
);
}
- return (
-
-
-
+ // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ──────────────────────────────
+ if (report) {
+ return (
+
+
{data.title || "未命名会商"}
+ 结果绘制 · 只读
+
+
+
+ {data.has_report ? (
+
+ ) : (
+
+ 暂无结果报告
+ 这次会商还没有生成第三步「结果绘制」的方案报告。
+
+ )}
+
+
+ );
+ }
+
+ // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)─────
+ const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`;
+
+ return (
+
+
+
+
+ {data.title || "未命名会商"}
+
+
只读分享
-
- 这是一份多智能体会商的结果报告快照(只读),你可以查看全部内容,但无法继续研讨。
+
+ 这是一份多智能体会商的结果分享(只读)。
-
- {data.has_report ? (
-
- ) : (
-
- 暂无结果报告
- 这次会商还没有生成第三步「结果绘制」的方案报告。
-
- )}
+
+
+
+ {/* 1. 结果绘制 —— 新标签打开报告页 */}
+
+
+
+ 结果绘制
+
+ {data.has_report ? (
+
+
+ 查看结果绘制
+
+
+ ) : (
+
+ 这次会商还没有生成第三步「结果绘制」的方案报告。
+
+ )}
+
+
+ {/* 2. 总控最终结论 */}
+
+
+
+ 总控最终结论
+
+ {data.consensus_text ? (
+
+
+ {data.consensus_text}
+
+
+ ) : (
+ 暂无总控结论。
+ )}
+
+
+ {/* 3. 各席位最终交付 */}
+
+
+
+ 各席位最终交付
+
+ {data.seat_deliveries.length}
+
+
+
+
+
);
diff --git a/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py b/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py
index df1c4c9e..e2a7b38b 100644
--- a/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py
+++ b/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py
@@ -91,12 +91,28 @@ class ImportResultResponse(BaseModel):
draft_id: str | None = Field(default=None, description="The new local draft id (when imported)")
+class SeatDeliveryItem(BaseModel):
+ """One sub-agent's final delivery from the Step 2 roundtable."""
+
+ sender: str = ""
+ content: str = ""
+
+
class PublicDraftShareSnapshotResponse(BaseModel):
- """Read-only roundtable-result snapshot served to anonymous viewers."""
+ """Read-only roundtable-result snapshot served to anonymous viewers.
+
+ Mirrors what the in-app Step 3「最终方案展示」(`HighFidelityReport`) shows, minus
+ the live per-seat output files (those need authenticated thread access):
+ 总控最终结论 + 各席位最终交付 + the generated 结果绘制 report.
+ """
share_code: str
title: str = Field(..., description="Draft title")
mode: str = Field(default="view")
+ # 总控最终结论 —— the coordinator's final consensus text (Step 2 lastLeaderContent).
+ consensus_text: str = Field(default="", description="Coordinator's final consensus (markdown)")
+ # 各席位最终交付 —— each sub-agent's final delivery, filtered from Step 2 dialogues.
+ seat_deliveries: list[SeatDeliveryItem] = Field(default_factory=list)
# The Step 3「结果绘制」report — a self-contained HTML document. Empty when the
# shared draft never reached / generated a result report.
html: str = Field(default="", description="Self-contained Step 3 result report HTML")
@@ -240,6 +256,40 @@ async def import_share(
# ---------------------------------------------------------------------------
+def _result_sections(draft: dict) -> tuple[str, list[dict[str, str]]]:
+ """Derive (总控最终结论, 各席位最终交付) from a draft's Step 2 snapshot.
+
+ Mirrors the frontend `Step3Panel`: the consensus is `step2.lastLeaderContent`
+ and each seat delivery is a Step 2 dialogue bubble tagged
+ ``confidence == "子智能体交付"``. Falls back to the active run's snapshot when
+ the top-level Step 2 fields are empty (multi-run drafts).
+ """
+ step2 = draft.get("step2") if isinstance(draft.get("step2"), dict) else {}
+ step2 = step2 or {}
+ consensus = step2.get("lastLeaderContent") or ""
+ dialogues = step2.get("step2RoundtableDialogues")
+ if not isinstance(dialogues, list):
+ dialogues = []
+
+ # Multi-run drafts keep the freshest content on the active run; use it when
+ # the top-level snapshot carries nothing.
+ if not consensus and not dialogues and isinstance(step2.get("runs"), list):
+ runs = [r for r in step2["runs"] if isinstance(r, dict)]
+ active_id = step2.get("activeRunId")
+ active = next((r for r in runs if r.get("id") == active_id), None) or (runs[0] if runs else None)
+ if active:
+ consensus = active.get("lastLeaderContent") or ""
+ d = active.get("step2RoundtableDialogues")
+ dialogues = d if isinstance(d, list) else []
+
+ seat_deliveries = [
+ {"sender": str(d.get("sender") or ""), "content": str(d.get("content") or "")}
+ for d in dialogues
+ if isinstance(d, dict) and d.get("confidence") == "子智能体交付"
+ ]
+ return consensus, seat_deliveries
+
+
@public_router.get(
"/{share_code}",
response_model=PublicDraftShareSnapshotResponse,
@@ -265,10 +315,13 @@ async def get_public_draft_share_snapshot(
step3 = draft.get("step3") if isinstance(draft.get("step3"), dict) else None
html = (step3 or {}).get("html") or ""
+ consensus_text, seat_deliveries = _result_sections(draft)
return PublicDraftShareSnapshotResponse(
share_code=share_code,
title=draft.get("title") or _UNTITLED,
mode="view",
+ consensus_text=consensus_text,
+ seat_deliveries=[SeatDeliveryItem(**s) for s in seat_deliveries],
html=html,
summary=(step3 or {}).get("summary"),
generated_at=(step3 or {}).get("generatedAt"),