Files
Cyrene_v2/internal/services/gateway/broadcast.go
T
AskaEth 4ea68d17f3 refactor(gateway+model): 拆 gateway/server.go 719→204 + 重构 scanSSE/processFrom 到 ≤80行
- server.go 204 + 新 routes.go(193,路由注册/Handler/withMiddleware)/deps.go(108)/contracts.go(231,17依赖接口)/broadcast.go(78,BroadcastPersisted/CapacityAwaken/Mood); 导出面不变; 74条路由pattern+RBAC门逐条比对一致
- model/openai_stream.go: scanSSE 138→25(parse/apply/finalize 对齐 anthropic_stream); openaiStreamState结构体
- channelbridge/inbound.go: processFrom 112→46(blacklistDrop/inboundTargetFor/runInboundTurn/deliverReply)
- 纯行为等价; build/vet/test绿(56包)+win/darwin交叉编译
2026-09-30 11:58:49 +08:00

79 lines
3.5 KiB
Go

package gateway
import (
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
)
// This file owns the gateway's outbound realtime pushes: the persisted-message
// frames shared with the channel bridge plus the per-persona state frames the
// frontend renders (mood badge, capacity awaken).
// BroadcastPersisted broadcasts a just-persisted chat message as a WS event so
// every browser of the same account appends it without a manual refresh
// (design/16 §4.2). It is shared by the orchestrator (Web turns) and the channel
// bridge (IM messages), so both paths flow through one frame shape. The event type
// is split by session: the main web session and its web sub-sessions broadcast
// `message_persisted` for web-chat realtime sync, while any other session id is an
// isolated channel adapter session (sess-xxx) and broadcasts `channel_persisted`
// for the IM adapter log page — the web chat has no handler for the latter, so it
// is naturally ignored there and never pollutes the conversation. Thinking is
// stripped to stay consistent with the WS history replay (design/16 §6). It is a
// no-op-safe call when the hub has broadcast wired.
func (s *Server) BroadcastPersisted(msg history.Message) {
wire := toHistoryMessage(msg)
wire.Thinking = ""
typ := "channel_persisted"
if msg.SessionID == history.MainSessionID || strings.HasPrefix(msg.SessionID, history.MainSessionID+":") {
typ = "message_persisted"
}
s.hub.broadcast(wsEvent{Type: typ, SessionID: msg.SessionID, Data: map[string]any{"message": wire}})
}
// wsCapacityAwaken is the WS event type for a capacity state transition
// (design/14 §5). Its data field is the frontend AwakenPayload {fromState,toState,tool}.
const wsCapacityAwaken = "capacity_awaken"
// BroadcastCapacityAwaken fans a capacity state transition to the Web UI as a
// `capacity_awaken` frame (design/14 §5). sessionID is the conversation that
// transitioned, so the client targets the correct session rather than treating the
// wake event as global. data matches the frontend AwakenPayload {fromState, toState,
// tool}; tool may be empty (no single tool was lit, the client then keeps the prior
// tool badge).
func (s *Server) BroadcastCapacityAwaken(sessionID, fromState, toState, tool string) {
s.hub.broadcast(wsEvent{
Type: wsCapacityAwaken,
SessionID: sessionID,
Data: map[string]any{
"fromState": fromState,
"toState": toState,
"tool": tool,
},
})
}
// wsMoodEvent is the WS event type carrying the persona's mood after a turn
// (design/07 §3 下行 `mood`; design/01 §2 情绪状态机). The frontend's existing
// `mood` handler (ws.ts) reads data.mood and shows the 心情 badge.
const wsMoodEvent = "mood"
// BroadcastMood fans one emotion change to every Web client as a `mood` frame
// (design/07 §3). It is called only when the mood actually changed — the
// machinery in the composition root is the gate — so the badge does not flicker
// with an unchanged value on every turn. data matches what the frontend `mood`
// handler reads ({mood}); affection/personaId ride along as the honest extra
// detail (affection is the persona's current 好感度, personaId the persona whose
// mood moved) and are ignored by clients that do not render them. The mood is a
// persona-level value, not a per-session one, so no sessionId is attached.
func (s *Server) BroadcastMood(personaID, mood string, affection int) {
s.hub.broadcast(wsEvent{
Type: wsMoodEvent,
Data: map[string]any{
"mood": mood,
"personaId": personaID,
"affection": affection,
},
})
}