import { useState } from "react"; import { useAppStore } from "../../store/useAppStore"; import { useAuditStore } from "../audit"; import { Notice, PageHeader } from "../components"; import { Button, EmptyState, Surface, Tag } from "../../components/primitives"; import type { BackthinkItem } from "../../lib/types"; import { Icon } from "../../components/Icon"; import { FlowGraphView } from "./FlowGraph"; import { adminBackthink } from "../../lib/api"; import { useAsyncData } from "../../lib/useAsyncData"; import { BACKTHINK_SESSION_ID } from "../../store/sessionCache"; /** 推送路由的中文标签(后端 PushResult.Route 的取值域)。未知取值原样显示, * 绝不猜一个更好听的词。 */ const PUSH_ROUTE_LABEL: Record = { active_client: "活跃客户端", im_bridge: "IM 桥", rate_limited: "限频拦截", none: "无投递目标", }; /** 推送路由的色调:真的送出去了才用 ok,被限频拦住是 warn。 */ function pushRouteTone(route: string): "ok" | "warn" | "neutral" { if (route === "active_client" || route === "im_bridge") return "ok"; if (route === "rate_limited") return "warn"; return "neutral"; } function BackthinkRow({ item }: { item: BackthinkItem }): React.ReactNode { const markPushed = useAppStore((s) => s.markBackthinkPushed); // 裁决 16.4.2:这条思考的链路图取自**后台思考会话**(`sess-backthink`,design/19 §1 // 的发布会话范围)的快照,而不是管理端 Flow 页那张「当前会话」图 —— 后者属于用户端 // 会话,与后台思考的完整链路是两件事,且不会被思考图顶掉。 const graph = useAppStore((s) => s.flowGraphs[BACKTHINK_SESSION_ID] ?? null); const wsStatus = useAppStore((s) => s.wsStatus); const [open, setOpen] = useState(false); return (
{new Date(item.at).toLocaleTimeString()} {item.trigger} {item.shouldNotify ? "需要打扰" : "静默"} {item.pushed ? "已推送" : "未推送"} {item.route ? {PUSH_ROUTE_LABEL[item.route] ?? item.route} : null} {item.bypassed ? 事件直发 : null}
{item.conclusion ? (
{item.conclusion}
) : null} {item.detail ? (
推送说明:{item.detail}
) : null} {open ? ( graph ? ( ) : (
{wsStatus === "connected" ? "后台思考会话尚未产生链路图 —— 该会话还没有 graph_update 快照(本轮思考未跑 span,或后端未发布)。" : `实时通道未连接(${wsStatus}),链路图无法更新 —— 这不等于「本轮思考没有链路」。`}
) ) : null}
); } export function BackthinkModule(): React.ReactNode { const backthink = useAppStore((s) => s.backthink); const log = useAuditStore((s) => s.log); const [filter, setFilter] = useState<"all" | "notify" | "silent">("all"); const [interveneHint, setInterveneHint] = useState(false); // Cold-load persisted history once (design/12 §4); real-time updates come from the // WS `backthink` events already feeding useAppStore.backthink (head-inserted), so we // do NOT poll-replace the array (which would re-render the whole list). Rows are keyed // by id → React reconciles incrementally, appending only NEW records. const rec = useAsyncData(() => adminBackthink(100), []); const byId = new Map(); for (const b of backthink) if (b.id) byId.set(b.id, b); for (const b of rec.data ?? []) if (b.id) byId.set(b.id, b); const items = Array.from(byId.values()) .sort((a, b) => (b.at ?? 0) - (a.at ?? 0)) .filter((b) => { if (filter === "notify") return b.shouldNotify === true; if (filter === "silent") return b.shouldNotify !== true; return true; }); return (
{ log("backthink.intervene", "error"); setInterveneHint(true); }} > 干预 } /> {interveneHint ? ( 对后台思考的打断 / 重派需后端 M10+ 能力接入,当前未启用;此处仅展示已产生的思考记录。 ) : null}
setFilter("all")}>全部 setFilter("notify")}>需打扰 setFilter("silent")}>静默
{items.length === 0 ? ( } title="暂无后台思考" hint="后台任务完成时按 WS 事件实时呈现(backthink / thinking)。" /> ) : (
{items.map((b) => ( ))}
)} 数据源:/ws/chat 下行 backthink / thinking 事件(实时,主驱动)+ 打开时一次拉取 GET /api/v1/admin/backthink 历史,两者按 id 去重、按时间倒序,新记录增量插入(不整页重渲)。「推送结果」的 route / detail / bypassed 两条路径同名同义:历史拉取取 AdminBackthinkItem 的同名键,实时帧取后端原始 PushResult 的 Route / Detail / Bypassed(解析器已读,见 lib/wsParse.ts#parseBackthinkItem,兼容 camelCase 变体),因此本次会话新到的行同样显示推送路由与推送说明,不再只显示「已推送/未推送」。链路导图取自后台思考会话(sess-backthink)的 graph_update 快照 —— 后端完整链路图始终发布,与管理端 Flow 页的「当前会话」图相互独立(裁决 16.4.2)。干预为后端 M10+ 提供,当前为只读展示。
); } function FilterPill({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }): React.ReactNode { return ( ); }