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
@@ -0,0 +1,107 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/orchestration"
|
||||
)
|
||||
|
||||
// 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 "voice_input":
|
||||
// Voice is not wired to a live pipeline yet; surface the payload on the
|
||||
// bus so an upper layer (or a later milestone) can consume it, rather
|
||||
// than dropping it as unknown.
|
||||
c.publishPublic(wsVoiceInput, rawData(in.Data))
|
||||
case "subagent_ask":
|
||||
c.handleSubagentAsk(in)
|
||||
case "task_output_query":
|
||||
c.handleTaskOutputQuery(in)
|
||||
default:
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": "unknown event type"}})
|
||||
}
|
||||
}
|
||||
|
||||
// subagentAskRequest is the decoded payload of a subagent_ask frame. Mode selects
|
||||
// the audit window (""/full/tail/keyword); n bounds a tail query; query is the
|
||||
// keyword for a keyword query.
|
||||
type subagentAskRequest struct {
|
||||
ID string `json:"id"`
|
||||
Mode string `json:"mode"`
|
||||
N int `json:"n"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// taskOutputRequest is the decoded payload of a task_output_query frame.
|
||||
type taskOutputRequest struct {
|
||||
TaskID string `json:"taskId"`
|
||||
}
|
||||
|
||||
// handleSubagentAsk audits a sub-agent's context and replies with a
|
||||
// ws.subagent_event frame carrying the real context window. When no auditor is
|
||||
// injected (gateway not wired with a scheduler) it replies with a clear error
|
||||
// rather than a fabricated "pending" snapshot.
|
||||
func (c *wsConn) handleSubagentAsk(in wsInbound) {
|
||||
if c.h.auditor == nil {
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": "subagent audit unavailable"}})
|
||||
return
|
||||
}
|
||||
var req subagentAskRequest
|
||||
_ = json.Unmarshal(in.Data, &req)
|
||||
if req.ID == "" {
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": "subagent id required"}})
|
||||
return
|
||||
}
|
||||
mode := orchestration.AuditMode(req.Mode)
|
||||
if mode == "" {
|
||||
mode = orchestration.AuditFull
|
||||
}
|
||||
res, err := c.h.auditor.AuditContext(context.Background(), req.ID, mode, req.N, req.Query)
|
||||
if err != nil {
|
||||
c.h.log.Debug("subagent audit", "id", req.ID, "err", err)
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": err.Error()}})
|
||||
return
|
||||
}
|
||||
c.enqueueJSON(wsEvent{Type: "subagent_event", Data: res})
|
||||
}
|
||||
|
||||
// handleTaskOutputQuery fetches the real task output for taskId and replies with
|
||||
// a ws.task_output frame. When no task store is injected it replies with a clear
|
||||
// error rather than an empty {}. A task that is still running returns the output
|
||||
// captured so far.
|
||||
func (c *wsConn) handleTaskOutputQuery(in wsInbound) {
|
||||
if c.h.taskOutputs == nil {
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": "task output unavailable"}})
|
||||
return
|
||||
}
|
||||
var req taskOutputRequest
|
||||
_ = json.Unmarshal(in.Data, &req)
|
||||
if req.TaskID == "" {
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": "task id required"}})
|
||||
return
|
||||
}
|
||||
ev, err := c.h.taskOutputs.TaskOutput(context.Background(), req.TaskID)
|
||||
if err != nil {
|
||||
c.h.log.Debug("task output query", "id", req.TaskID, "err", err)
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: map[string]any{"message": err.Error()}})
|
||||
return
|
||||
}
|
||||
text := ev.Output.Stdout
|
||||
if ev.Output.Stderr != "" {
|
||||
if text != "" {
|
||||
text += "\n"
|
||||
}
|
||||
text += ev.Output.Stderr
|
||||
}
|
||||
c.enqueueJSON(wsEvent{Type: "task_output", Data: map[string]any{
|
||||
"taskId": ev.ID,
|
||||
"output": text,
|
||||
"status": string(ev.Status),
|
||||
}})
|
||||
}
|
||||
Reference in new issue
Block a user