【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。
176 lines
7.1 KiB
Go
176 lines
7.1 KiB
Go
package gateway
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations"
|
|
)
|
|
|
|
// IoT device-control and automation-rule admin writes (owner-only, audited).
|
|
// Device control performs a real control on the owning provider; the others
|
|
// answer honestly when no scenario/rule store is wired.
|
|
|
|
// iotControlRequest is the POST /api/v1/admin/iot/{id}/control body.
|
|
type iotControlRequest struct {
|
|
Action string `json:"action"`
|
|
Params map[string]any `json:"params"`
|
|
}
|
|
|
|
// adminIoTControl sends a control instruction to a device through the IoT
|
|
// manager, which applies it on the owning provider and publishes a device_update
|
|
// event. A real action only when the deployment has a device provider.
|
|
func (s *Server) adminIoTControl(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
var req iotControlRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Action) == "" {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "action is required"))
|
|
return
|
|
}
|
|
mgr, ok := s.iot.(*iot.Manager)
|
|
if !ok {
|
|
s.auditClaims(r, "iot.control", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "iot_unavailable", "IoT 服务未配置"))
|
|
return
|
|
}
|
|
dev, err := mgr.Control(r.Context(), id, req.Action, req.Params)
|
|
if err != nil {
|
|
s.auditClaims(r, "iot.control", "error")
|
|
writeError(w, mapIotError(err))
|
|
return
|
|
}
|
|
if dev == nil {
|
|
s.auditClaims(r, "iot.control", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "device %q not found", id))
|
|
return
|
|
}
|
|
s.auditClaims(r, "iot.control", "ok")
|
|
writeJSON(w, http.StatusOK, iotDeviceSummary{ID: dev.ID, Name: dev.Name, Type: dev.Type, State: dev.State, Online: dev.Online})
|
|
}
|
|
|
|
// adminIoTScenario answers honestly: the IoT subsystem has no scenario store yet
|
|
// (it owns device list/control/status only), so scenario write lands in M10.
|
|
func (s *Server) adminIoTScenario(w http.ResponseWriter, r *http.Request) {
|
|
s.auditClaims(r, "iot.scenario", "error")
|
|
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "IoT 场景存储为 M10 提供(当前 IoT 仅有设备列表/控制/状态)", HTTPCode: http.StatusNotImplemented})
|
|
}
|
|
|
|
// adminIoTRule answers honestly: no IoT automation-rule store exists yet.
|
|
func (s *Server) adminIoTRule(w http.ResponseWriter, r *http.Request) {
|
|
s.auditClaims(r, "iot.rule", "error")
|
|
writeError(w, &apperr.AppError{Code: "not_implemented", Message: "IoT 自动化规则存储为 M10 提供(当前 IoT 仅有设备列表/控制/状态)", HTTPCode: http.StatusNotImplemented})
|
|
}
|
|
|
|
// mapIotError maps iot sentinel errors to HTTP errors.
|
|
func mapIotError(err error) *apperr.AppError {
|
|
switch {
|
|
case errors.Is(err, iot.ErrDeviceNotFound):
|
|
return apperr.Wrap(apperr.ErrNotFound, "not_found", "device not found")
|
|
case errors.Is(err, iot.ErrUnsupported):
|
|
return apperr.Wrap(apperr.ErrBadRequest, "unsupported_action", "action not supported")
|
|
case errors.Is(err, iot.ErrOffline):
|
|
return apperr.Wrap(apperr.ErrConflict, "device_offline", "device offline")
|
|
case errors.Is(err, iot.ErrInvalidParam):
|
|
return apperr.Wrap(apperr.ErrBadRequest, "invalid_param", "invalid parameter")
|
|
default:
|
|
return apperr.Wrap(apperr.ErrInternal, "iot_control_failed", "device control failed")
|
|
}
|
|
}
|
|
|
|
// automationCreateRequest is the POST /api/v1/admin/automations body.
|
|
type automationCreateRequest struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"` // cron | event
|
|
Expression string `json:"expression"`
|
|
}
|
|
|
|
// adminCreateAutomation registers a real cron/event rule whose action records its
|
|
// fire. Invalid cron expressions or malformed rules are answered honestly.
|
|
func (s *Server) adminCreateAutomation(w http.ResponseWriter, r *http.Request) {
|
|
var req automationCreateRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Kind) == "" || strings.TrimSpace(req.Expression) == "" {
|
|
s.auditClaims(r, "automation.create", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "name, kind and expression are required"))
|
|
return
|
|
}
|
|
auto, ok := s.automations.(*automations.Service)
|
|
if !ok {
|
|
s.auditClaims(r, "automation.create", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
|
return
|
|
}
|
|
id, err := auto.AddRuleByName(r.Context(), req.Name, req.Kind, req.Expression)
|
|
if err != nil {
|
|
s.auditClaims(r, "automation.create", "error")
|
|
if errors.Is(err, automations.ErrInvalidCron) {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "invalid_cron", "invalid cron expression"))
|
|
return
|
|
}
|
|
if errors.Is(err, automations.ErrInvalidRule) {
|
|
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "invalid_rule", "invalid rule"))
|
|
return
|
|
}
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "failed to add automation"))
|
|
return
|
|
}
|
|
s.auditClaims(r, "automation.create", "ok")
|
|
writeJSON(w, http.StatusCreated, map[string]any{"id": id, "name": req.Name, "kind": req.Kind})
|
|
}
|
|
|
|
// adminRunAutomation manually triggers a registered rule's action once.
|
|
func (s *Server) adminRunAutomation(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
auto, ok := s.automations.(*automations.Service)
|
|
if !ok {
|
|
s.auditClaims(r, "automation.run", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
|
return
|
|
}
|
|
out, err := auto.RunRule(r.Context(), id)
|
|
if err != nil {
|
|
s.auditClaims(r, "automation.run", "error")
|
|
if errors.Is(err, automations.ErrNotFound) {
|
|
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "automation %q not found", id))
|
|
return
|
|
}
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "failed to run automation"))
|
|
return
|
|
}
|
|
s.auditClaims(r, "automation.run", "ok")
|
|
writeJSON(w, http.StatusOK, map[string]any{"id": id, "output": out})
|
|
}
|
|
|
|
// adminDeleteAutomation removes a registered rule (cron or event) by id.
|
|
func (s *Server) adminDeleteAutomation(w http.ResponseWriter, r *http.Request) {
|
|
id := r.PathValue("id")
|
|
auto, ok := s.automations.(*automations.Service)
|
|
if !ok {
|
|
s.auditClaims(r, "automation.delete", "error")
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
|
return
|
|
}
|
|
if err := auto.RemoveRule(r.Context(), id); err != nil {
|
|
s.auditClaims(r, "automation.delete", "error")
|
|
if errors.Is(err, automations.ErrNotFound) {
|
|
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "automation %q not found", id))
|
|
return
|
|
}
|
|
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "failed to delete automation"))
|
|
return
|
|
}
|
|
s.auditClaims(r, "automation.delete", "ok")
|
|
writeJSON(w, http.StatusOK, map[string]any{"id": id, "deleted": true})
|
|
}
|