feat(gateway): 地基③ 打通实时信号 producer→前端WS + loop 真流式 + 上行路由补全
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

打通前端 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:
Aska committed 2026-09-06 15:53:48 +08:00
1 parent 04bfa2c7a7
commit 9b207a31e8
14 files changed
+673 -109

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
package main
import "context"
// streamChunkSubject is the bus subject the loop's OnStream closure publishes
// each token delta to. The gateway hub subscribes to ws.> and re-broadcasts it
// as a ws.stream_chunk frame to every client.
const streamChunkSubject = "ws.stream_chunk"
// streamChunk is the loop's OnStream callback. It publishes one token delta to
// the gateway's streaming subject so the hub fans it out to every client as a
// ws.stream_chunk frame. It is a no-op when no bus is wired and never blocks the
// loop: bus.Publish is asynchronous and a full per-subscription buffer drops the
// event rather than stalling the model stream.
func (a *app) streamChunk(delta string) {
if a.bus == nil {
return
}
if err := a.bus.Publish(context.Background(), streamChunkSubject, map[string]any{"delta": delta}); err != nil {
a.log.Warn("stream chunk publish", "err", err)
}
}
+16 -12
View File
@@ -44,8 +44,8 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/telegram"
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/webhook"
"git.yeij.top/AskaEth/Cyrene_v2/plugins/channels/weixin_ilink"
iotools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io"
"git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/basic"
iotools "git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io"
)
// defaultOwnerPath and defaultPersonasDir point at the on-disk persona/owner
@@ -302,6 +302,7 @@ func (a *app) wireRuntime() error {
PersonaID: personaID,
MaxRounds: defaultMaxRounds,
WorkingMemory: memory.NewWorkingMemory(0),
OnStream: a.streamChunk,
Log: logger.WithSource(a.log, "runtime/loop"),
})
@@ -331,7 +332,7 @@ func (a *app) wireAuxiliary() error {
}
a.tasks = tmgr
iotMgr, err := iot.New(a.cfg.IoT, a.log)
iotMgr, err := iot.New(a.cfg.IoT, a.log, iot.WithBus(a.bus))
if err != nil {
return fmt.Errorf("iot: %w", err)
}
@@ -404,16 +405,19 @@ func (a *app) wireChannels() error {
func (a *app) wireFrontend() error {
jwt := auth.New(a.cfg.Auth)
gw, err := gateway.New(a.cfg, gateway.Deps{
Log: logger.WithSource(a.log, "gateway"),
Auth: jwt,
Bus: a.bus,
Registry: a.caps,
Loop: a.loop,
Session: a.sessions,
Personas: a.personas,
Memories: a.memStore,
IoT: a.iot,
Frontend: resolveFrontendFS(a.log),
Log: logger.WithSource(a.log, "gateway"),
Auth: jwt,
Bus: a.bus,
Registry: a.caps,
Loop: a.loop,
Session: a.sessions,
Personas: a.personas,
Memories: a.memStore,
IoT: a.iot,
Scheduler: a.scheduler,
Tasks: a.tasks,
Streaming: true,
Frontend: resolveFrontendFS(a.log),
})
if err != nil {
return fmt.Errorf("gateway: %w", err)