Files
Cyrene_v2/internal/services/gateway/ws_route.go
T
Aska 6dbb4a2e18
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat: 唤醒式架构批次——capacity/L4/会话编排/管理面板写CRUD/前端集成(77-81)
【capacity 唤醒式核心(77)】internal/runtime/capacity:(Decider L1启发式+L2上下文+人格覆盖→三态静息/唤醒/全能→允许工具集+capacity_awaken事件)。loop 外围允许集过滤(不硬阻断=能力≠边界红线)。config 加 Capacity(Enabled/MinConfidence/ToolSets)。wire buildCapacity 接入 loop。

【L4 记忆驱动意图(80)】recall.go:从记忆召回用户习惯(搞一下xx=办事)→唤醒L1/L2漏掉/升置信;群聊陌生人不读owner习惯(隔离红线);RecordIntent闭环(判定写回记忆越用越准);默认保守静息。

【会话消息编排(78)】gateway/session_orchestrator.go:每会话(模式/pending有界5/tail上下文尾巴20);Submit非阻塞(idle立即/busy入队ack/满拒绝);完成回调按序续接(tail+新消息合并增量注入非全量重放);Interrupt中断;REST 202/409+WS session_busy/pending_ack事件;不重造任务引擎复用loop。

【前端集成(79)】ws.ts 加 capacity_awaken/session_busy/pending_ack 解析;store 加 awakenState/sessionBusy;TopBar/ChatShell 加唤醒态徽标+忙态提示(不阻塞输入框);状态组件 status.tsx;Miuix风格。

【管理面板写CRUD(81)】20个写端点(POST/PUT/DELETE /admin/*,owner+审计落库):personas(真持久化YAML)/users apikey(签发掩码+撤销)/iot控制(真下发)/automations(建/触发/删)/llm(test真调用+临时)/channels(connect/close/身份测试actor白名单)/plugins(卸载)/memories(search真检索)/sessions(export)。无存储如实501不伪造。拆4文件满足≤500红线。前端9模块接真写。

【wire收口】buildCapacity接入loop+OnCapacity事件;server.go newOrchestrator接hub。

实机验证:iot ac.living control 200 真实改变 on:true;observability/metrics 200、health ok;全仓 build/vet/test 47包绿+前端tsc/vite。
2026-09-06 21:54:40 +08:00

113 lines
3.7 KiB
Go

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 "interrupt":
// design/15 §3: user says "停下/算了" → cancel the in-flight turn and
// clear the session's pending queue, so the next message starts fresh.
c.h.handleInterrupt(in.Data)
case "voice_input":
// Voice routes through the injected STT→chat→TTS pipeline. Without a
// voice service it replies with a clear error rather than dropping the
// frame. The audio reply is delivered as a voice_output event on the
// requesting connection.
c.handleVoiceInput(in)
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),
}})
}