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
@@ -175,3 +175,51 @@ func (g *RunGraph) Snapshot() *FlowGraph {
|
||||
EndedAt: g.ended,
|
||||
}
|
||||
}
|
||||
|
||||
// Clone 深拷贝一份图,供**锁外**的读者使用(契约 v0.71 的
|
||||
// `GET /api/v1/admin/flow/graph` 就近取材于 `loop.FlowPublisher.LastGraph`)。
|
||||
//
|
||||
// 为什么必须深拷贝:`Snapshot` 只复制节点结构体,`Props` 映射与 `Log` 切片
|
||||
// 仍与活图**共享底层存储**;而 `RunGraph.Upsert` 对同一个节点是**就地**合并
|
||||
// (`merge` 往同一个 map 写键、往同一个切片追加)。发布路径在持有发布器
|
||||
// 互斥锁时就把图编码掉了,因此共享无碍;但只读查询把图交出去之后还要在
|
||||
// 锁外编码,与下一帧 span 的合并并发 ⇒ 会读到撕裂的 map(Go 里并发读写
|
||||
// map 直接是致命错误)。深拷贝把这份共享切断,且只发生在人为的只读查询上,
|
||||
// 不在实时发布路径上,因此不影响每帧开销。
|
||||
func (g *FlowGraph) Clone() *FlowGraph {
|
||||
if g == nil {
|
||||
return nil
|
||||
}
|
||||
out := &FlowGraph{
|
||||
ID: g.ID,
|
||||
Nodes: make([]FlowNode, 0, len(g.Nodes)),
|
||||
Edges: append([]FlowEdge(nil), g.Edges...),
|
||||
SessionID: g.SessionID,
|
||||
StartedAt: g.StartedAt,
|
||||
EndedAt: g.EndedAt,
|
||||
}
|
||||
for _, n := range g.Nodes {
|
||||
cp := n
|
||||
if n.Props != nil {
|
||||
cp.Props = make(map[string]any, len(n.Props))
|
||||
for k, v := range n.Props {
|
||||
cp.Props[k] = v
|
||||
}
|
||||
}
|
||||
if n.Log != nil {
|
||||
cp.Log = make([]Event, len(n.Log))
|
||||
for i, e := range n.Log {
|
||||
ev := e
|
||||
if e.Props != nil {
|
||||
ev.Props = make(map[string]any, len(e.Props))
|
||||
for k, v := range e.Props {
|
||||
ev.Props[k] = v
|
||||
}
|
||||
}
|
||||
cp.Log[i] = ev
|
||||
}
|
||||
}
|
||||
out.Nodes = append(out.Nodes, cp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user