feat(roundtable): 会商前端调用报错收集 + 诊断页用户名/范围切换

- 前端调用报错收集(用户实际遇到的报错,服务端诊断看不到的也收):新增
  POST /api/roundtable-diagnostics/report(非 admin,scope="client")+ 前端上报器
  roundtable-planning/api/diagnostics-report.ts。streamMultiAgent(第二步席位 +
  第三步报告/结构化/绘制/大屏)覆盖 HTTP 错误(502)、SSE error 帧、worker error 帧
  (模型没调用成功)、fetch 网络不可达、流式中途中断,reported 去重;initMultiAgent /
  intent / recommend / 后台作业接口同步上报。
- 诊断页:每条报错显示上报用户名(👤,user_id→邮箱@前缀,后端 best-effort 解析);
  第一个筛选改为「后台挂起 / 前台调用」切换(前台调用=foreground,client,后端 scope
  支持逗号多值),阶段下拉随范围联动(后台→二三步;前台→一二三步)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-19 23:45:34 +08:00
1 parent 4a403c95c5
commit 762d3620c4
12 files changed
+433 -18

No files matched your search

@@ -55,7 +55,37 @@ const STAGE_LABELS: Record<string, string> = {
const SCOPE_LABELS: Record<string, string> = {
background: "后台挂起",
foreground: "前台实时",
foreground: "前台实时(服务端)",
client: "前端上报",
};
// 范围切换:后台挂起(background) / 前台调用(foreground 服务端观测 + client 前端上报)。
type ScopeCategory = "background" | "frontend";
// 每个范围下可选的阶段(按用户要求:后台挂起只到第二/三步;前台调用含第一步)。
const STAGE_OPTIONS: Record<ScopeCategory, string[]> = {
background: [
"step2_init",
"step2_leader",
"step2_seat",
"step2_orchestration",
"step3_summary",
"step3_report",
"step3_dashboard",
"step3_structure",
"job",
],
frontend: [
"step1_intent",
"step2_init",
"step2_leader",
"step2_seat",
"step2_recommend",
"step3_summary",
"step3_report",
"step3_dashboard",
"step3_structure",
],
};
const LEVEL_STYLES: Record<string, string> = {
@@ -101,6 +131,11 @@ function DiagnosticRow({ item }: { item: DiagnosticItem }) {
{item.agentName ? (
<span className="text-xs text-slate-500 dark:text-muted-foreground">· {item.agentName}</span>
) : null}
{item.userName ? (
<span className="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-600 dark:bg-slate-800 dark:text-slate-300">
👤 {item.userName}
</span>
) : null}
<span className="ml-auto text-xs text-slate-400 dark:text-muted-foreground">{fmtTime(item.createdAt)}</span>
</div>
<p className="mt-1 whitespace-pre-wrap break-words text-slate-700 dark:text-foreground">
@@ -133,7 +168,7 @@ export default function RoundtableDiagnosticsPage() {
const isAdmin = getStoredAuth()?.system_role === "admin";
const navigate = useNavigate();
const [scope, setScope] = useState<string>(ALL);
const [scopeCategory, setScopeCategory] = useState<ScopeCategory>("background");
const [stage, setStage] = useState<string>(ALL);
const [level, setLevel] = useState<string>(ALL);
const [event, setEvent] = useState<string>(ALL);
@@ -141,9 +176,12 @@ export default function RoundtableDiagnosticsPage() {
const [q, setQ] = useState("");
const [page, setPage] = useState(1);
// 「前台调用」= 服务端观测(foreground) + 前端上报(client) 两种 scope 合并查(后端按逗号分隔多值)。
const scopeParam = scopeCategory === "background" ? "background" : "foreground,client";
const filters: DiagnosticFilters = useMemo(
() => ({
scope: scope === ALL ? undefined : scope,
scope: scopeParam,
stage: stage === ALL ? undefined : stage,
level: level === ALL ? undefined : level,
event: event === ALL ? undefined : event,
@@ -151,7 +189,7 @@ export default function RoundtableDiagnosticsPage() {
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
}),
[scope, stage, level, event, q, page],
[scopeParam, stage, level, event, q, page],
);
const { data, isLoading, isFetching, refetch } = useRoundtableDiagnostics(filters, isAdmin);
@@ -169,6 +207,13 @@ export default function RoundtableDiagnosticsPage() {
setPage(1);
};
// 切换范围时清空阶段筛选(两种范围的阶段选项不同)+ 回到第一页。
const switchScope = (next: ScopeCategory) => {
setScopeCategory(next);
setStage(ALL);
setPage(1);
};
const applySearch = () => resetPageAnd(() => setQ(qInput.trim()));
return (
@@ -199,10 +244,26 @@ export default function RoundtableDiagnosticsPage() {
{/* Filters */}
<div className="flex flex-wrap items-center gap-2 border-b border-slate-200 dark:border-border bg-white dark:bg-card px-4 py-2">
<FilterSelect label="范围" value={scope} onChange={(v) => resetPageAnd(() => setScope(v))}
options={facets?.scope ?? []} labels={SCOPE_LABELS} />
{/* 范围切换:后台挂起 / 前台调用 */}
<div className="inline-flex rounded-lg border border-slate-200 dark:border-border p-0.5">
{(["background", "frontend"] as ScopeCategory[]).map((c) => (
<button
key={c}
type="button"
onClick={() => switchScope(c)}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition-colors",
scopeCategory === c
? "bg-slate-800 text-white dark:bg-primary dark:text-primary-foreground"
: "text-slate-500 hover:text-slate-700 dark:text-muted-foreground dark:hover:text-foreground",
)}
>
{c === "background" ? "后台挂起" : "前台调用"}
</button>
))}
</div>
<FilterSelect label="阶段" value={stage} onChange={(v) => resetPageAnd(() => setStage(v))}
options={facets?.stage ?? []} labels={STAGE_LABELS} />
options={STAGE_OPTIONS[scopeCategory]} labels={STAGE_LABELS} />
<FilterSelect label="级别" value={level} onChange={(v) => resetPageAnd(() => setLevel(v))}
options={facets?.level ?? []} />
<FilterSelect label="事件" value={event} onChange={(v) => resetPageAnd(() => setEvent(v))}