Files
Cyrene_v2/internal/services/gateway/admin_write_iot_automations.go
T
AskaEth 2dee372fb2
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink/admin): ③b — rag/delete 真实现 + D1 推送出口钩子(自建优先→个推兜底) + api-reference 补 D2/D3/D5 补丁
- B5: POST /admin/rag/delete 真做(DeleteDocument→DeleteProject 级联删章节+索引清理, owner+审计, 200/404/500, 500降级兜底); users/iot scenarios+rules 诚实501(无用户表/IoT场景规则模型, 消息写明缺什么, 不伪造), 锁住 501+not_implemented
- D1: backthink/push_outlet.go PushExporter{Export}; pushOutlet 自建HTTP(2xx=送达, token仅Authorization头+error脱敏)优先→Getui兜底(未接入, 诚实日志'getui not integrated'+(false,nil)); backthink.Deps.Exporter → pusher.withExporter, Push 在WS/IM后 exportAsync(detached+5s超时, 失败仅日志, 不破坏现有投递, 限流不上报); config PushOutletConfig(PUSH_EXPORTER_* + GETUI_*占位)
- 前端 Rag.tsx/api.ts 删除后 reload + 类型; Docs/api-client v0.31 + api-reference(#23 owner/200-404, §8.4, 端点表PATCH/DELETE messages + pinned + messages?q=)
- 测试(push_outlet/admin_rag_delete/admin_honest501/knowledge store), 后端+前端build全绿+3平台交叉编译
- 刻意: Getui SDK 未接入(无SDK不硬造), 主人自建服务填 PUSH_EXPORTER_URL/TOKEN 即可
2026-09-09 00:08:29 +08:00

188 lines
7.5 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 owns device
// list/control/status only (the Provider interface has no scenario model or
// store), so there is nothing to persist a scenario into. It reports the missing
// store rather than fabricating one.
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 场景未实现:IoT 子系统仅有设备列表/控制/状态(Provider 接口无场景模型与存储);场景 CRUD 需先新增模型 + 执行器",
HTTPCode: http.StatusNotImplemented,
})
}
// adminIoTRule answers honestly: no IoT automation-rule model or store exists
// (the generic automations service runs cron/event rules, not device-state
// conditions with device actions), so a rule cannot be persisted or executed.
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 自动化规则未实现:IoT 子系统无规则模型与存储(通用 automations 服务只跑 cron/事件规则,不含设备状态条件 + 设备动作执行器)",
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})
}