feat(gateway): 链路图冷启动快照(v0.71)+ 面板来源说明条 —— 修「轮次结束后打开页面一片空白」
- 断点已证 = **时序层**:graph_update 只走实时帧从不重放、flowGraphs 不持久化、补拉通路此前根本不存在(EndRound 注释承诺过「刷新能重放」,代码里从来没有)
铁证:15:16:38 页面重载抹掉 15:09 那轮图,而下一轮心跳要等 ~15:24 ⇒ 打开后台思考页时桶必然为空
- 新增只读端点 GET /api/v1/admin/flow/graph?sessionId=(owner|admin;省略=主会话;graph 与 graph_update 帧的 data 逐字同形)
无图 ⇒ 200 + graph:null(不画假图);图源未接线 ⇒ 500 flow_unavailable(不伪装成「没有图」);不审计、不发布
- loop.FlowPublisher.LastGraph:只读快照(不发布/不改图/不动轮次计数),FlowGraph.Clone 深拷贝避免锁外编码撕裂
- 前端 useFlowSnapshot:只在「本会话还没有图」时补拉(避免每行一个请求);Flow/Agents/Backthink 三处接入 + 来源说明条(明确「最近一轮」与「多轮历史回放尚未实现」)
- 变异实测:LastGraph 恒 nil ⇒ 4 条测试红;白名单去掉 sess-backthink ⇒ 对应测试红(首次变异因未使用 import 编译失败 ⇒ 已作废重做)
- ⚠️ 诚实边界:每会话只回**最近一轮**(多轮回放属 F2);白名单外会话(IM 群聊/陌生人)永远没有图;**后端重启后未跑新轮次前端点如实返 null(图是内存资产,不落库)**—— 主人要的「本地数据」正是 F2 要补的
This commit is contained in:
1 parent
a3482ed06e
commit
1bf85a955b
23 files changed
+1427
-36
No files matched your search
@@ -1,12 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useAppStore } from "../../store/useAppStore";
|
||||
import type { AgentInfo, AgentStatus } from "../../lib/types";
|
||||
import type { AgentInfo, AgentStatus, FlowGraph } from "../../lib/types";
|
||||
import { Notice, PageHeader } from "../components";
|
||||
import { Button, EmptyState, Surface, Tag, type TagTone } from "../../components/primitives";
|
||||
import { sendUp } from "../../lib/ws";
|
||||
import { useAuditStore } from "../audit";
|
||||
import { Icon } from "../../components/Icon";
|
||||
import { FlowGraphView } from "./FlowGraph";
|
||||
import { FlowSnapshotHint } from "./flow/FlowSnapshotHint";
|
||||
import { useFlowSnapshot, type FlowGraphSource } from "./flow/useFlowSnapshot";
|
||||
import { MAIN_SESSION_ID } from "../../store/sessionCache";
|
||||
|
||||
const statusTone: Record<AgentStatus, { label: string; tone: TagTone }> = {
|
||||
idle: { label: "空闲", tone: "neutral" },
|
||||
@@ -68,6 +71,10 @@ function AgentCard({ agent }: { agent: AgentInfo }): React.ReactNode {
|
||||
|
||||
export function AgentsModule(): React.ReactNode {
|
||||
const agents = useAppStore((s) => s.agents);
|
||||
const activeSessionId = useAppStore((s) => s.activeSessionId);
|
||||
// 与 Flow 页同一条补拉(契约 v0.71):本页读的也是「激活会话」的图,
|
||||
// 刷新过页面后实时帧已经错过,不补拉就永远停在空态。
|
||||
const graphSource = useFlowSnapshot(activeSessionId || MAIN_SESSION_ID);
|
||||
const flowGraph = useAppStore((s) => s.flowGraph);
|
||||
const taskLogs = useAppStore((s) => s.taskLogs);
|
||||
const clearTaskLog = useAppStore((s) => s.clearTaskLog);
|
||||
@@ -105,13 +112,11 @@ export function AgentsModule(): React.ReactNode {
|
||||
<TabPill active={tab === "sub"} onClick={() => setTab("sub")}>子代理 ({subAgents.length})</TabPill>
|
||||
</div>
|
||||
|
||||
{flowGraph ? (
|
||||
<Surface gap="sm">
|
||||
<FlowGraphView graph={flowGraph} />
|
||||
</Surface>
|
||||
) : (
|
||||
<Notice>暂无流程图数据 —— 连接 /ws/chat 后按 graph_update 事件实时渲染。</Notice>
|
||||
)}
|
||||
<AgentsFlowBlock
|
||||
graph={flowGraph}
|
||||
sessionId={activeSessionId || MAIN_SESSION_ID}
|
||||
source={graphSource}
|
||||
/>
|
||||
|
||||
{list.length === 0 ? (
|
||||
<EmptyState icon={<Icon name="agent" size={32} />} title={tab === "main" ? "无主会话 Agent" : "暂无子代理"} />
|
||||
@@ -146,8 +151,34 @@ export function AgentsModule(): React.ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
function TabPill({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }): React.ReactNode {
|
||||
/** 流程图这一块(有图就画、没图就诚实空态)。props 驱动(不读 store),
|
||||
* 因此可被真实渲染测试直接喂两种输入(见 flowSnapshot.render.test.ts 头注释)。
|
||||
*
|
||||
* 空态文案里必须说明**两条**来源都查过(实时帧 + 最近一轮快照,契约 v0.71):
|
||||
* 否则用户分不清「本会话没跑过带 span 的轮次」与「图源没接线」。 */
|
||||
export function AgentsFlowBlock({
|
||||
graph,
|
||||
sessionId,
|
||||
source,
|
||||
}: {
|
||||
graph: FlowGraph | null;
|
||||
sessionId: string;
|
||||
source: FlowGraphSource;
|
||||
}): React.ReactNode {
|
||||
if (!graph) {
|
||||
return (
|
||||
<Notice>暂无流程图数据 —— 本会话还没有跑过带 span 的轮次(实时帧与「最近一轮快照」都为空)。发起一次会话后此处会显示链路图。</Notice>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Surface gap="sm">
|
||||
<FlowGraphView graph={graph} />
|
||||
<FlowSnapshotHint sessionId={graph.sessionId ?? sessionId} source={source ?? "live"} />
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
function TabPill({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }): React.ReactNode { return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`rounded-[var(--mu-radius)] px-3 py-1.5 text-xs font-medium transition-all duration-200 ease-[var(--mu-spring)] ${
|
||||
|
||||
Reference in new issue
Block a user