【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。
113 lines
3.7 KiB
Go
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),
|
|
}})
|
|
}
|