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
@@ -25,6 +25,9 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration/comm"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/task"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||||
)
|
||||
|
||||
@@ -192,11 +195,31 @@ type stubIoT struct {
|
||||
|
||||
func (s *stubIoT) List(ctx context.Context) ([]iot.Device, error) { return s.list, s.err }
|
||||
|
||||
// stubAuditor mimics SubagentAuditor for the subagent_ask route.
|
||||
type stubAuditor struct {
|
||||
res *orchestration.AuditResult
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stubAuditor) AuditContext(ctx context.Context, id string, mode orchestration.AuditMode, n int, query string) (*orchestration.AuditResult, error) {
|
||||
return s.res, s.err
|
||||
}
|
||||
|
||||
// stubTaskOutputs mimics TaskOutputStore for the task_output_query route.
|
||||
type stubTaskOutputs struct {
|
||||
ev task.OutputEvent
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *stubTaskOutputs) TaskOutput(ctx context.Context, id string) (task.OutputEvent, error) {
|
||||
return s.ev, s.err
|
||||
}
|
||||
|
||||
// stubChannel implements sdk.Channel.
|
||||
type stubChannel struct{}
|
||||
|
||||
func (stubChannel) Name() string { return "qq" }
|
||||
func (stubChannel) Connect(ctx context.Context) error { return nil }
|
||||
func (stubChannel) Connect(ctx context.Context) error { return nil }
|
||||
func (stubChannel) Disconnect(ctx context.Context) error { return nil }
|
||||
func (stubChannel) IsConnected() bool { return true }
|
||||
func (stubChannel) HealthCheck(ctx context.Context) error { return nil }
|
||||
@@ -205,9 +228,9 @@ var _ sdk.Channel = stubChannel{}
|
||||
|
||||
// captureRunner records the sessionID each Run call sees.
|
||||
type captureRunner struct {
|
||||
got []string
|
||||
res *loop.Result
|
||||
err error
|
||||
got []string
|
||||
res *loop.Result
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *captureRunner) Run(ctx context.Context, sessionID string, messages []model.Message) (*loop.Result, error) {
|
||||
@@ -664,8 +687,8 @@ func TestStatusRecorderFlush(t *testing.T) {
|
||||
func TestSPA(t *testing.T) {
|
||||
t.Parallel()
|
||||
front := fstest.MapFS{
|
||||
"index.html": &fstest.MapFile{Data: []byte("<html>root</html>")},
|
||||
"assets/app.js": &fstest.MapFile{Data: []byte("var x = 1;")},
|
||||
"index.html": &fstest.MapFile{Data: []byte("<html>root</html>")},
|
||||
"assets/app.js": &fstest.MapFile{Data: []byte("var x = 1;")},
|
||||
}
|
||||
srv, ts, _ := buildEnv(t, cfgDefault(), Deps{Frontend: front})
|
||||
_ = srv
|
||||
@@ -825,20 +848,48 @@ func TestHubRelayChatBranch(t *testing.T) {
|
||||
func TestHubRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
hb := newTestHub(nil, bus.New())
|
||||
hb.auditor = &stubAuditor{res: &orchestration.AuditResult{
|
||||
AgentID: "sub-1", Status: orchestration.StatusRunning, Goal: "do it", Mode: orchestration.AuditFull,
|
||||
Messages: []model.Message{model.TextMessage(model.RoleUser, "hi")},
|
||||
}}
|
||||
hb.taskOutputs = &stubTaskOutputs{ev: task.OutputEvent{
|
||||
ID: "task-1", Status: task.StatusDone, Output: task.Output{Stdout: "out", Stderr: "err"},
|
||||
}}
|
||||
c := newFakeConn(hb, 32)
|
||||
hb.register(c)
|
||||
|
||||
// subagent_ask → subagent_event (enqueueJSON).
|
||||
c.route(wsInbound{Type: "subagent_ask"})
|
||||
// subagent_ask → subagent_event (real audit data, not a hardcoded pending).
|
||||
c.route(wsInbound{Type: "subagent_ask", Data: json.RawMessage(`{"id":"sub-1"}`)})
|
||||
if ev := readFakeFrame(t, c); ev.Type != "subagent_event" {
|
||||
t.Fatalf("subagent frame type = %q, want subagent_event", ev.Type)
|
||||
}
|
||||
// task_output_query → task_output (enqueueJSON).
|
||||
c.route(wsInbound{Type: "task_output_query"})
|
||||
// subagent_ask with no auditor → clear error, never a fabricated pending.
|
||||
hb.auditor = nil
|
||||
c.route(wsInbound{Type: "subagent_ask", Data: json.RawMessage(`{"id":"sub-1"}`)})
|
||||
if ev := readFakeFrame(t, c); ev.Type != "error" {
|
||||
t.Fatalf("no-auditor subagent frame type = %q, want error", ev.Type)
|
||||
}
|
||||
hb.auditor = &stubAuditor{res: &orchestration.AuditResult{}}
|
||||
|
||||
// task_output_query → task_output with real output.
|
||||
c.route(wsInbound{Type: "task_output_query", Data: json.RawMessage(`{"taskId":"task-1"}`)})
|
||||
if ev := readFakeFrame(t, c); ev.Type != "task_output" {
|
||||
t.Fatalf("task frame type = %q, want task_output", ev.Type)
|
||||
}
|
||||
// unknown → error (enqueueJSON), with non-nil rawData via mode_switch path already.
|
||||
// task_output_query with no task store → clear error, never an empty {}.
|
||||
hb.taskOutputs = nil
|
||||
c.route(wsInbound{Type: "task_output_query", Data: json.RawMessage(`{"taskId":"task-1"}`)})
|
||||
if ev := readFakeFrame(t, c); ev.Type != "error" {
|
||||
t.Fatalf("no-task store frame type = %q, want error", ev.Type)
|
||||
}
|
||||
hb.taskOutputs = &stubTaskOutputs{ev: task.OutputEvent{ID: "task-1", Status: task.StatusDone, Output: task.Output{Stdout: "out"}}}
|
||||
|
||||
// voice_input → publishes to bus (no frame enqueued, not an unknown error).
|
||||
c.route(wsInbound{Type: "voice_input", Data: json.RawMessage(`{"audio":"x"}`)})
|
||||
if err := connHasNoFrame(c); err != nil {
|
||||
t.Fatalf("voice_input should not enqueue a frame directly: %v", err)
|
||||
}
|
||||
// unknown → error (enqueueJSON).
|
||||
c.route(wsInbound{Type: "nope"})
|
||||
if ev := readFakeFrame(t, c); ev.Type != "error" {
|
||||
t.Fatalf("unknown frame type = %q, want error", ev.Type)
|
||||
@@ -850,6 +901,53 @@ func TestHubRoute(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubBridgeDeviceUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := bus.New()
|
||||
hb := newTestHub(nil, b)
|
||||
c := newFakeConn(hb, 32)
|
||||
hb.register(c)
|
||||
hb.start()
|
||||
t.Cleanup(hb.close)
|
||||
|
||||
if err := b.Publish(context.Background(), "iot.device_update", map[string]any{"message": "灯开了"}); err != nil {
|
||||
t.Fatalf("publish: %v", err)
|
||||
}
|
||||
if ev := readFakeFrame(t, c); ev.Type != "device_update" {
|
||||
t.Fatalf("type = %q, want device_update (frame=%+v)", ev.Type, ev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHubBridgeSubagentProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
hb := newTestHub(nil, nil)
|
||||
c := newFakeConn(hb, 32)
|
||||
hb.register(c)
|
||||
|
||||
if err := hb.relay(context.Background(), bus.Event{
|
||||
Subject: "cyrene.agent.orchestrator.event",
|
||||
Payload: comm.Event{Name: orchestration.EventDone, Data: orchestration.Progress{
|
||||
AgentID: "sub-1", Status: orchestration.StatusDone, RoleID: "engineer", Mode: "chat",
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatalf("relay: %v", err)
|
||||
}
|
||||
if ev := readFakeFrame(t, c); ev.Type != "subagent_event" {
|
||||
t.Fatalf("type = %q, want subagent_event (frame=%+v)", ev.Type, ev)
|
||||
}
|
||||
|
||||
// An internal ask subject must never surface to clients.
|
||||
if err := hb.relay(context.Background(), bus.Event{
|
||||
Subject: "cyrene.agent.a.b.ask",
|
||||
Payload: comm.Event{Name: orchestration.EventDone, Data: orchestration.Progress{AgentID: "sub-1"}},
|
||||
}); err != nil {
|
||||
t.Fatalf("relay ask: %v", err)
|
||||
}
|
||||
if err := connHasNoFrame(c); err != nil {
|
||||
t.Fatalf("internal ask must not be broadcast: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishPublicNilBus(t *testing.T) {
|
||||
t.Parallel()
|
||||
hb := newTestHub(nil, nil) // nil bus
|
||||
|
||||
Reference in new issue
Block a user