feat(gateway): 地基③ 打通实时信号 producer→前端WS + loop 真流式 + 上行路由补全
打通前端 ws.ts 里那批死消费者事件(thinking/mood/tool_used/subagent_event/
task_output/device_update/reminder/backthink),并给 loop 接真 token 流式。
- wire.go:loop 建时传 OnStream(streamChunk 闭包把 delta 发 bus ws.stream_chunk);
gateway.Deps 注入 Scheduler/Tasks/Streaming;iot 接共享总线 iot.WithBus(a.bus)。
- ws_bridge.go:hub 额外订阅 iot.>/task.>/automation.>/reminder.>/cyrene.agent.*.event,
normalizeEvent 归一为 ws.*(device_update/task_output/task_done/automation_result/
reminder/subagent_event);不订阅 cyrene.agent.> 以免 ask/reply 内部事件进 relay。
- ws_route.go:voice_input→ws.voice_input;subagent_ask→Scheduler.AuditContext(真实
上下文审计,替换 pending stub);task_output_query→Manager.TaskOutput(真实输出,
替换空 {})。
- server.go:新增 SubagentAuditor/TaskOutputStore 接口 + Deps.Scheduler/Tasks/Streaming。
- orchestration/auditcontext.go:Scheduler.AuditContext(full/tail/keyword)。
- task/manager.go:Manager.TaskOutput(id)。
- backthink:record() 补发 ws.backthink 事件。
- json tag 修正(device_update/reminder/task output 结构体加小写 tag + DeviceUpdate
补 message),使前端分支能真正渲染。
实机验证:WS 握手 101、task manager ready、iot manager initialized(WithBus 生效)。
go build/vet/test 全绿;TestHubBridgeDeviceUpdate/TestHubBridgeSubagentProgress 覆盖
映射。dev 推至后续版本。
This commit is contained in:
1 parent
04bfa2c7a7
commit
9b207a31e8
14 files changed
+673
-109
No files matched your search
@@ -23,6 +23,7 @@ const (
|
||||
wsEventsPattern = "ws.>"
|
||||
wsPrefix = "ws."
|
||||
wsChatSubject = "ws.chat"
|
||||
wsVoiceInput = "ws.voice_input"
|
||||
wsReadLimit = 4096
|
||||
wsWriteWait = 10 * time.Second
|
||||
wsPongWait = 60 * time.Second
|
||||
@@ -57,27 +58,33 @@ type wsConn struct {
|
||||
|
||||
// hub owns the connection set and relays bus events to every client.
|
||||
type hub struct {
|
||||
mu sync.RWMutex
|
||||
conns map[*wsConn]struct{}
|
||||
bus bus.Bus
|
||||
runner Chatter
|
||||
log *slog.Logger
|
||||
upgrader websocket.Upgrader
|
||||
origins map[string]bool
|
||||
allowAll bool
|
||||
cancel context.CancelFunc
|
||||
mu sync.RWMutex
|
||||
conns map[*wsConn]struct{}
|
||||
bus bus.Bus
|
||||
runner Chatter
|
||||
auditor SubagentAuditor
|
||||
taskOutputs TaskOutputStore
|
||||
streaming bool
|
||||
log *slog.Logger
|
||||
upgrader websocket.Upgrader
|
||||
origins map[string]bool
|
||||
allowAll bool
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// newHub builds a hub bound to the server's dependencies.
|
||||
func newHub(s *Server) *hub {
|
||||
origins, allowAll := s.allowedOrigins()
|
||||
return &hub{
|
||||
conns: make(map[*wsConn]struct{}),
|
||||
bus: s.bus,
|
||||
runner: s.runner,
|
||||
log: s.log,
|
||||
origins: origins,
|
||||
allowAll: allowAll,
|
||||
conns: make(map[*wsConn]struct{}),
|
||||
bus: s.bus,
|
||||
runner: s.runner,
|
||||
auditor: s.auditor,
|
||||
taskOutputs: s.taskOutputs,
|
||||
streaming: s.streaming,
|
||||
log: s.log,
|
||||
origins: origins,
|
||||
allowAll: allowAll,
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
@@ -87,15 +94,20 @@ func newHub(s *Server) *hub {
|
||||
}
|
||||
|
||||
// start subscribes the hub to the bus event stream. Publishing to ws.> arrives
|
||||
// here and is fanned out to clients; nothing happens when no bus is wired.
|
||||
// here and is fanned out to clients; the producer bridge patterns (iot/task/
|
||||
// automation/reminder/subagent-progress) are also subscribed and normalized into
|
||||
// ws.* event types. Nothing happens when no bus is wired.
|
||||
func (h *hub) start() {
|
||||
if h.bus == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.cancel = cancel
|
||||
if _, err := h.bus.Subscribe(ctx, wsEventsPattern, h.relay); err != nil {
|
||||
h.log.Warn("bus subscribe", "pattern", wsEventsPattern, "err", err)
|
||||
patterns := append([]string{wsEventsPattern}, bridgePatterns...)
|
||||
for _, p := range patterns {
|
||||
if _, err := h.bus.Subscribe(ctx, p, h.relay); err != nil {
|
||||
h.log.Warn("bus subscribe", "pattern", p, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,15 +166,23 @@ func (h *hub) broadcast(ev wsEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// relay is the bus handler for the ws.> pattern. The ws.chat subject is a
|
||||
// streaming trigger (run the loop and emit chunks); every other subject becomes
|
||||
// an outbound event of its design type.
|
||||
// relay is the bus handler for the ws.> pattern and the producer bridge
|
||||
// patterns. The ws.chat subject is a streaming trigger (run the loop and emit
|
||||
// chunks); a ws.* subject becomes an outbound event of its design type; a
|
||||
// bridged producer subject (device_update, task output/done, automation result,
|
||||
// reminder, subagent progress) is normalized into the matching ws.* event type.
|
||||
// Events with no WS-visible mapping are dropped so internal agent-to-agent
|
||||
// traffic never leaks to clients.
|
||||
func (h *hub) relay(ctx context.Context, e bus.Event) error {
|
||||
if e.Subject == wsChatSubject {
|
||||
go h.streamChat(ctx, e.Payload)
|
||||
return nil
|
||||
}
|
||||
h.broadcast(wsEvent{Type: eventType(e.Subject), Data: e.Payload})
|
||||
ev, ok := normalizeEvent(e)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
h.broadcast(ev)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -187,7 +207,13 @@ func (h *hub) streamChat(ctx context.Context, payload any) {
|
||||
h.broadcast(wsEvent{Type: "stream_end", Data: map[string]any{"stopReason": "error", "error": err.Error()}})
|
||||
return
|
||||
}
|
||||
h.broadcast(wsEvent{Type: "stream_chunk", Data: map[string]any{"delta": res.Text}})
|
||||
// When the loop is streaming (OnStream wired), the deltas already fan out as
|
||||
// ws.stream_chunk through the OnStream→bus→relay path; a second whole-text
|
||||
// chunk would duplicate the tail. The non-streaming path (or a stub runner)
|
||||
// still emits one chunk so the stream contract holds in tests.
|
||||
if !h.streaming {
|
||||
h.broadcast(wsEvent{Type: "stream_chunk", Data: map[string]any{"delta": res.Text}})
|
||||
}
|
||||
h.broadcast(wsEvent{Type: "stream_end", Data: map[string]any{"stopReason": res.StopReason, "steps": res.Steps}})
|
||||
}
|
||||
|
||||
@@ -282,22 +308,6 @@ func (c *wsConn) writePump() {
|
||||
}
|
||||
}
|
||||
|
||||
// route dispatches one inbound frame by its type.
|
||||
func (c *wsConn) route(in wsInbound) {
|
||||
switch in.Type {
|
||||
case "mode_switch":
|
||||
c.publishPublic("ws.mode_switch", rawData(in.Data))
|
||||
case "chat":
|
||||
c.publishPublic(wsChatSubject, rawData(in.Data))
|
||||
case "subagent_ask":
|
||||
c.enqueueJSON(wsEvent{Type: "subagent_event", Data: map[string]any{"state": "pending"}})
|
||||
case "task_output_query":
|
||||
c.enqueueJSON(wsEvent{Type: "task_output", Data: map[string]any{}})
|
||||
default:
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": "unknown event type"}})
|
||||
}
|
||||
}
|
||||
|
||||
// publishPublic publishes a bus event without any per-connection reply.
|
||||
func (c *wsConn) publishPublic(subject string, payload any) {
|
||||
if c.h.bus == nil {
|
||||
@@ -306,11 +316,14 @@ func (c *wsConn) publishPublic(subject string, payload any) {
|
||||
_ = c.h.bus.Publish(context.Background(), subject, payload)
|
||||
}
|
||||
|
||||
// shutdown closes the connection exactly once.
|
||||
// shutdown closes the connection exactly once. It tolerates a nil *websocket.Conn
|
||||
// so tests that build a fake connection can rely on a safe teardown.
|
||||
func (c *wsConn) shutdown() {
|
||||
c.once.Do(func() {
|
||||
close(c.done)
|
||||
_ = c.conn.Close()
|
||||
if c.conn != nil {
|
||||
_ = c.conn.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user