Files
Cyrene_v2/internal/services/gateway/ws_bridge.go
T
Aska 9b207a31e8
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 推至后续版本。
2026-09-06 15:53:48 +08:00

93 lines
3.2 KiB
Go

package gateway
import (
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/comm"
)
// eventBridge maps a producer bus subject to its outbound WS event type. The hub
// subscribes to these (and re-normalizes into ws.* types) so the frontend's
// existing consumers light up without the producers changing their subjects.
var eventBridge = map[string]string{
"iot.device_update": "device_update",
"task.output": "task_output",
"task.done": "task_done",
"automation.result": "automation_result",
"reminder.trigger": "reminder",
}
// bridgePatterns are the additional bus patterns the hub subscribes to on top of
// ws.> to capture producer events and normalize them for WS delivery.
var bridgePatterns = []string{
"iot.>",
"task.>",
"automation.>",
"reminder.>",
"cyrene.agent.*.event",
}
// normalizeEvent maps a bus event to an outbound wsEvent. A ws.* subject keeps
// its design type (the ws. prefix is stripped); a known producer subject is
// bridged to its ws.* type; a sub-agent progress event is shaped into
// ws.subagent_event. It returns ok=false for events that have no WS-visible
// representation (e.g. internal cyrene.agent ask/reply traffic).
func normalizeEvent(e bus.Event) (wsEvent, bool) {
if typ, ok := eventBridge[e.Subject]; ok {
return wsEvent{Type: typ, Data: e.Payload}, true
}
if strings.HasPrefix(e.Subject, wsPrefix) {
return wsEvent{Type: eventType(e.Subject), Data: e.Payload}, true
}
if ev, ok := subagentProgressEvent(e); ok {
return ev, true
}
return wsEvent{}, false
}
// subagentProgressEvent maps an orchestration progress event — broadcast on
// cyrene.agent.<from>.event via comm.PublishEvent — into a ws.subagent_event
// frame. Only the five sub-agent progress names are surfaced; any other event
// broadcast on the same subject (ask/reply/addressed events, or foreign event
// names) is ignored so internal agent-to-agent traffic stays internal.
func subagentProgressEvent(e bus.Event) (wsEvent, bool) {
if !isBroadcastAgentEvent(e.Subject) {
return wsEvent{}, false
}
ev, ok := e.Payload.(comm.Event)
if !ok {
return wsEvent{}, false
}
switch ev.Name {
case orchestration.EventDispatched, orchestration.EventStarted,
orchestration.EventDone, orchestration.EventFailed, orchestration.EventAborted:
default:
return wsEvent{}, false
}
p, ok := ev.Data.(orchestration.Progress)
if !ok {
return wsEvent{}, false
}
return wsEvent{Type: "subagent_event", Data: map[string]any{
"id": p.AgentID,
"name": p.AgentID,
"status": string(p.Status),
"role": p.RoleID,
"mode": p.Mode,
}}, true
}
// isBroadcastAgentEvent reports whether subject has the exact 4-token broadcast
// shape cyrene.agent.<from>.event, which is how the scheduler publishes progress.
// It deliberately does not match addressed events (cyrene.agent.<from>.<to>.event)
// or the ask/reply subjects.
func isBroadcastAgentEvent(subject string) bool {
parts := strings.Split(subject, ".")
if len(parts) != 4 {
return false
}
return parts[0] == "cyrene" && parts[1] == "agent" && parts[3] == "event"
}