Files
Cyrene_v2/internal/services/gateway/admin_flow.go
T
AskaEth 1bf85a955b
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 要补的
2026-10-05 15:38:53 +08:00

71 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}