package gateway import ( "net/http" "strings" apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" ) // mainSessionID 是未指定 `?sessionId=` 时的默认会话 —— Web 主会话 // (`internal/runtime/history.MainSessionID`)。它与网关其余默认会话 // 口径同源(见 ws_hub_turn.go 的 streamChat),因此不新增第二个「默认会话」 // 常量。 const mainSessionID = history.MainSessionID // flowUnavailableCode 是图源未接线时的机器可读错误码。它**不**回 // graph:null:那会把「图源没接线」伪装成「这一轮确实没有图」,与 // usage_unavailable / channel_events_unavailable 同一口径。 const flowUnavailableCode = "flow_unavailable" // adminFlowGraphResponse 是 GET /api/v1/admin/flow/graph 的响应体(契约 v0.71)。 // // Graph 为 nil 表示「该会话确实还没有图」——这是一个**诚实的空态**,不是错误: // 客户端必须据此渲染空态,绝不允许补画任何节点或假图(§4.11(b)-3)。 // 该键**恒在**(`null` 而不是缺席),使客户端不必区分「没有这个键」与 // 「这个键是空」。 type adminFlowGraphResponse struct { SessionID string `json:"sessionId"` // Graph 是**最近一轮**的图快照,与 `graph_update` 帧的 data 逐字同形 // (契约 §3.4.5 / §4.11(a))。字段以 `any` 承载,因为「没有图」(nil ⇒ // 线上 JSON `null`)与「有图」的区分就是本字段的语义。 Graph any `json:"graph"` } // adminFlowGraph 是 `GET /api/v1/admin/flow/graph` 的处理器(契约 v0.71, // design/19 §4 的 P3「多轮历史回放」的最小一步)。 // // 它回答的是主人第二次报的那个洞:`graph_update` **只走实时帧、从不重放**, // 而前端图桶(`flowGraphs`)**不持久化** ⇒ 「轮次结束后才打开页面」或 // 「刷新过页面」永远空着。本端点让打开页面时能**只读拉一次**最近一轮的图。 // // 三条硬约束(与 §4.11(b) 逐条对应): // 1. **只读**:经 `FlowGraphSource` 拿快照,网关不发布、不改图、不动任何会话; // 2. **不画假图**:该会话没有图 ⇒ `graph:null`;图源未接线 ⇒ `500 flow_unavailable` // (**不是** graph:null,见 flowUnavailableCode); // 3. **只回最近一轮**:更早轮次的历史回放仍未实现(design/19 §4 P3)—— // 因此面板必须在图上说明「这不是你要的那一行那轮的图」。 // // 角色门:`writeAuth`(owner|admin),与 design/19 §3「flow 可视化仅 owner/admin」 // 一致;它与同一张图的 WS 下行帧(`graph_update` 落 frameReachesAll 的 fail-closed // 默认档 = 只发 owner+admin)是同一档,因此**不存在**「帧看不到、REST 看得到」的 // 旁路。本端点只读、不审计。 func (s *Server) adminFlowGraph(w http.ResponseWriter, r *http.Request) { sessionID := strings.TrimSpace(r.URL.Query().Get("sessionId")) if sessionID == "" { sessionID = mainSessionID } if s.flow == nil { s.log.Warn("flow graph source not wired", "session", sessionID) writeError(w, apperr.Wrap(apperr.ErrInternal, flowUnavailableCode, "flow graph source is not wired")) return } // Graph 恒在响应里(没有图时是 `null`),见 adminFlowGraphResponse 的注释。 var graph any if snap := s.flow.LastGraph(sessionID); snap != nil { graph = snap } writeJSON(w, http.StatusOK, adminFlowGraphResponse{SessionID: sessionID, Graph: graph}) }