Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
fc5eaa2941
4 files changed
+253
-31
No files matched your search
@@ -64,11 +64,15 @@ export function App() {
|
|||||||
<Route path="/landing" element={<LandingPage />} />
|
<Route path="/landing" element={<LandingPage />} />
|
||||||
{/* Public read-only conversation viewer — no authentication. */}
|
{/* Public read-only conversation viewer — no authentication. */}
|
||||||
<Route path="/share/:shareCode" element={<PublicSharePage />} />
|
<Route path="/share/:shareCode" element={<PublicSharePage />} />
|
||||||
{/* Public read-only roundtable result viewer — no authentication. */}
|
{/* Public read-only roundtable share — landing + full result report. */}
|
||||||
<Route
|
<Route
|
||||||
path="/roundtable-share/:shareCode"
|
path="/roundtable-share/:shareCode"
|
||||||
element={<PublicRoundtableSharePage />}
|
element={<PublicRoundtableSharePage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/roundtable-share/:shareCode/report"
|
||||||
|
element={<PublicRoundtableSharePage report />}
|
||||||
|
/>
|
||||||
{/* Public read-only scheduled-task runs viewer — no authentication. */}
|
{/* Public read-only scheduled-task runs viewer — no authentication. */}
|
||||||
<Route
|
<Route
|
||||||
path="/public/scheduled-tasks/:taskId"
|
path="/public/scheduled-tasks/:taskId"
|
||||||
|
|||||||
@@ -22,11 +22,21 @@ export interface DraftShare {
|
|||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One sub-agent's final delivery from the Step 2 roundtable. */
|
||||||
|
export interface PublicDraftSeatDelivery {
|
||||||
|
sender: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Read-only roundtable-result snapshot returned by the public view-share endpoint. */
|
/** Read-only roundtable-result snapshot returned by the public view-share endpoint. */
|
||||||
export interface PublicDraftShareSnapshot {
|
export interface PublicDraftShareSnapshot {
|
||||||
share_code: string;
|
share_code: string;
|
||||||
title: string;
|
title: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
|
/** 总控最终结论 (markdown). */
|
||||||
|
consensus_text: string;
|
||||||
|
/** 各席位最终交付. */
|
||||||
|
seat_deliveries: PublicDraftSeatDelivery[];
|
||||||
/** Self-contained Step 3 result report HTML (empty when no report exists). */
|
/** Self-contained Step 3 result report HTML (empty when no report exists). */
|
||||||
html: string;
|
html: string;
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
|
|||||||
@@ -1,21 +1,100 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Loader2 } from "lucide-react";
|
import {
|
||||||
|
BarChart3,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
import { getPublicDraftShareSnapshot } from "@/core/roundtable-draft-shares/api";
|
import {
|
||||||
|
getPublicDraftShareSnapshot,
|
||||||
|
type PublicDraftSeatDelivery,
|
||||||
|
} from "@/core/roundtable-draft-shares/api";
|
||||||
|
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||||
|
|
||||||
|
/** 各席位最终交付 —— 折叠手风琴(仿 HighFidelityReport 第二区块)。 */
|
||||||
|
function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) {
|
||||||
|
const [expanded, setExpanded] = useState<Set<number>>(() => new Set());
|
||||||
|
const toggle = (i: number) =>
|
||||||
|
setExpanded((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(i)) next.delete(i);
|
||||||
|
else next.add(i);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (seats.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="text-[11px] text-slate-400 italic">本次会商暂无子智能体交付。</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{seats.map((seat, idx) => {
|
||||||
|
const open = expanded.has(idx);
|
||||||
|
return (
|
||||||
|
<div key={`${seat.sender}-${idx}`} className="border border-slate-200 rounded-xl overflow-hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggle(idx)}
|
||||||
|
className="w-full flex items-center justify-between gap-2 px-3 py-2.5 bg-slate-50 hover:bg-slate-100 transition text-left"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
{open ? (
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-slate-500 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3.5 h-3.5 text-slate-500 shrink-0" />
|
||||||
|
)}
|
||||||
|
<span className="text-[11.5px] font-bold text-slate-800 truncate">
|
||||||
|
{seat.sender || "未命名席位"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-slate-400 font-mono shrink-0">
|
||||||
|
{seat.content.length} 字
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{open && (
|
||||||
|
<div className="px-4 py-3 text-xs text-slate-700 leading-relaxed border-t border-slate-100 bg-white">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
|
{seat.content || "(空)"}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public, read-only roundtable-result viewer.
|
* Public, read-only roundtable-result viewer.
|
||||||
*
|
*
|
||||||
* Reachable at `#/roundtable-share/<code>` with NO authentication — anyone with
|
* Reachable at `#/roundtable-share/<code>` with NO authentication — anyone with
|
||||||
* the link can open it. It renders the Step 3「结果绘制」report (a self-contained
|
* the link can open it. Two views, driven by the `report` prop / route:
|
||||||
* 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
|
* - landing (`#/roundtable-share/<code>`): a read-only result page mirroring the
|
||||||
* the app origin.
|
* in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」
|
||||||
|
* button that opens the full report in a NEW TAB (when one was generated).
|
||||||
|
* - report (`#/roundtable-share/<code>/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 { shareCode } = useParams<{ shareCode: string }>();
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
@@ -46,37 +125,113 @@ export default function PublicRoundtableSharePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ──────────────────────────────
|
||||||
<div className="bg-background flex h-screen flex-col">
|
if (report) {
|
||||||
<header className="flex shrink-0 flex-col gap-1 border-b px-6 py-3">
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="bg-background flex h-screen flex-col">
|
||||||
|
<header className="flex shrink-0 items-center gap-2 border-b px-6 py-3">
|
||||||
<h1 className="truncate text-base font-semibold">
|
<h1 className="truncate text-base font-semibold">
|
||||||
{data.title || "未命名会商"}
|
{data.title || "未命名会商"}
|
||||||
</h1>
|
</h1>
|
||||||
<span className="bg-muted text-muted-foreground shrink-0 rounded-full px-2 py-0.5 text-xs">
|
<span className="bg-muted text-muted-foreground shrink-0 rounded-full px-2 py-0.5 text-xs">
|
||||||
|
结果绘制 · 只读
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<main className="min-h-0 flex-1">
|
||||||
|
{data.has_report ? (
|
||||||
|
<iframe
|
||||||
|
// No `allow-scripts` — the report is self-contained (inline CSS +
|
||||||
|
// inline SVG charts) and must never run as a script in the app origin.
|
||||||
|
sandbox=""
|
||||||
|
title={data.title || "会商结果报告"}
|
||||||
|
srcDoc={data.html}
|
||||||
|
className="size-full border-0 bg-white"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm">
|
||||||
|
<p className="text-base font-medium text-foreground">暂无结果报告</p>
|
||||||
|
<p>这次会商还没有生成第三步「结果绘制」的方案报告。</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)─────
|
||||||
|
const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[#fef6f6] flex h-screen flex-col text-slate-800">
|
||||||
|
<header className="flex shrink-0 flex-col gap-1 border-b border-slate-100 bg-white px-6 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h1 className="truncate text-base font-semibold">
|
||||||
|
{data.title || "未命名会商"}
|
||||||
|
</h1>
|
||||||
|
<span className="bg-slate-100 text-slate-500 shrink-0 rounded-full px-2 py-0.5 text-xs">
|
||||||
只读分享
|
只读分享
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-slate-400 text-xs">
|
||||||
这是一份多智能体会商的结果报告快照(只读),你可以查看全部内容,但无法继续研讨。
|
这是一份多智能体会商的结果分享(只读)。
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-0 flex-1">
|
|
||||||
{data.has_report ? (
|
<main className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<iframe
|
<div className="mx-auto w-full max-w-6xl space-y-6 p-6">
|
||||||
// No `allow-scripts` — the report is self-contained (inline CSS + inline
|
{/* 1. 结果绘制 —— 新标签打开报告页 */}
|
||||||
// SVG charts) and must never run as a script in the app origin.
|
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||||
sandbox=""
|
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||||
title={data.title || "会商结果报告"}
|
<BarChart3 className="w-4 h-4 text-primary" />
|
||||||
srcDoc={data.html}
|
结果绘制
|
||||||
className="size-full border-0 bg-white"
|
</h4>
|
||||||
/>
|
{data.has_report ? (
|
||||||
) : (
|
<a
|
||||||
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm">
|
href={reportUrl}
|
||||||
<p className="text-base font-medium text-foreground">暂无结果报告</p>
|
target="_blank"
|
||||||
<p>这次会商还没有生成第三步「结果绘制」的方案报告。</p>
|
rel="noopener noreferrer"
|
||||||
</div>
|
className="bg-primary text-white hover:bg-[var(--brand-900)] inline-flex items-center gap-2 rounded-xl px-5 py-2.5 text-sm font-semibold transition shadow-sm shadow-primary/10"
|
||||||
)}
|
>
|
||||||
|
<BarChart3 className="size-4" />
|
||||||
|
查看结果绘制
|
||||||
|
<ExternalLink className="size-3.5 opacity-80" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-slate-400 italic">
|
||||||
|
这次会商还没有生成第三步「结果绘制」的方案报告。
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 2. 总控最终结论 */}
|
||||||
|
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||||
|
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||||
|
总控最终结论
|
||||||
|
</h4>
|
||||||
|
{data.consensus_text ? (
|
||||||
|
<div className="text-xs text-slate-700 leading-relaxed">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
|
{data.consensus_text}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-slate-400 italic">暂无总控结论。</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 3. 各席位最终交付 */}
|
||||||
|
<section className="bg-white border border-slate-200 rounded-2xl p-5 shadow-[0_2px_8px_rgba(0,0,0,0.02)]">
|
||||||
|
<h4 className="text-xs font-black text-slate-900 uppercase tracking-widest mb-3 flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-primary" />
|
||||||
|
各席位最终交付
|
||||||
|
<span className="text-[10px] font-mono font-semibold text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
||||||
|
{data.seat_deliveries.length}
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
<SeatDeliveries seats={data.seat_deliveries} />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,12 +91,28 @@ class ImportResultResponse(BaseModel):
|
|||||||
draft_id: str | None = Field(default=None, description="The new local draft id (when imported)")
|
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):
|
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
|
share_code: str
|
||||||
title: str = Field(..., description="Draft title")
|
title: str = Field(..., description="Draft title")
|
||||||
mode: str = Field(default="view")
|
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
|
# The Step 3「结果绘制」report — a self-contained HTML document. Empty when the
|
||||||
# shared draft never reached / generated a result report.
|
# shared draft never reached / generated a result report.
|
||||||
html: str = Field(default="", description="Self-contained Step 3 result report HTML")
|
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(
|
@public_router.get(
|
||||||
"/{share_code}",
|
"/{share_code}",
|
||||||
response_model=PublicDraftShareSnapshotResponse,
|
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
|
step3 = draft.get("step3") if isinstance(draft.get("step3"), dict) else None
|
||||||
html = (step3 or {}).get("html") or ""
|
html = (step3 or {}).get("html") or ""
|
||||||
|
consensus_text, seat_deliveries = _result_sections(draft)
|
||||||
return PublicDraftShareSnapshotResponse(
|
return PublicDraftShareSnapshotResponse(
|
||||||
share_code=share_code,
|
share_code=share_code,
|
||||||
title=draft.get("title") or _UNTITLED,
|
title=draft.get("title") or _UNTITLED,
|
||||||
mode="view",
|
mode="view",
|
||||||
|
consensus_text=consensus_text,
|
||||||
|
seat_deliveries=[SeatDeliveryItem(**s) for s in seat_deliveries],
|
||||||
html=html,
|
html=html,
|
||||||
summary=(step3 or {}).get("summary"),
|
summary=(step3 or {}).get("summary"),
|
||||||
generated_at=(step3 or {}).get("generatedAt"),
|
generated_at=(step3 or {}).get("generatedAt"),
|
||||||
|
|||||||
Reference in new issue
Block a user