feat(gateway,admin-web): Skill 运行时安装 + backthink 打扰原因 + 建提醒入口(P0-19/P0-20/P0-22,审计 §4.3)
- P0-19:POST /admin/plugins/install 支持 `kind:"skill"`(此前一律 400)——yaml 或 path 二选一、 name 一致性校验、显式错误码(bad_request/name_mismatch/skill_parse_failed/name_conflict)、 装后以 kind:"skill" 出现在 GET /admin/plugins;**文件来源解析错误绝不回显文件内容**(变异验证实测确认 该泄露向量真实存在:yaml 解码器会带出出错值前几字符 ⇒ 只回显路径 + 通用原因,细节进日志) - P0-20:AdminBackthinkItem 补 route/detail/bypassed(DB 早已存,零迁移),面板 Backthink 展示 - P0-22:新增 POST /admin/automations/reminders(owner-only + 审计),at/message 必填、RFC3339 校验、 409 already_exists;过去时间不作 400(下次轮询立即触发) - 顺手修真缺陷:新建 id 原用 time.Now().UnixNano(),Windows 时钟粒度粗到毫秒 ⇒ 并行创建撞 id 得假 409; 改用 crypto/rand 128 位 hex(仓库既有 skills/store.go 范式) - 复核:gateway 包 build/vet/测试绿 + 前端 tsc 0 错;新增 12 用例(含 8 条非法输入显式码); **4 条变异反证**均已回滚 - 未做(登记):WS 帧侧的 route/detail 未在前端展示(`lib/wsParse.ts` 3 行,需放行);skill 未落 skills.Store(重启后消失,注入点在 cmd/cyrene);提醒无删除入口(CancelReminder 已存在、无路由)
This commit is contained in:
1 parent
3638ed7708
commit
decf59c1b4
12 files changed
+956
-24
No files matched your search
@@ -1,10 +1,14 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
|
||||
@@ -185,3 +189,82 @@ func (s *Server) adminDeleteAutomation(w http.ResponseWriter, r *http.Request) {
|
||||
s.auditClaims(r, "automation.delete", "ok")
|
||||
writeJSON(w, http.StatusOK, map[string]any{"id": id, "deleted": true})
|
||||
}
|
||||
|
||||
// reminderCreateRequest 是 POST /api/v1/admin/automations/reminders 的请求体。
|
||||
// At 是 RFC3339(带小数秒亦可);ID 省略时由后端生成,使面板无需
|
||||
// 自己保证唯一。
|
||||
type reminderCreateRequest struct {
|
||||
ID string `json:"id"`
|
||||
At string `json:"at"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// adminCreateReminder 创建一条真实的定时提醒(design/10 §3 #12「提醒」)。
|
||||
// 提醒进 ReminderStore 后由既有的轮询循环到点发布 reminder.trigger
|
||||
// 到进程内 bus(P0-22 之前该 Store 只有测试调用,生产没有创建入口,
|
||||
// 因此提醒功能实际不可达)。at 若已是过去时刻,按 Store 语义会在
|
||||
// 下一次轮询立即触发 —— 这是真实行为,不是假成功。
|
||||
func (s *Server) adminCreateReminder(w http.ResponseWriter, r *http.Request) {
|
||||
var req reminderCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
|
||||
return
|
||||
}
|
||||
message := strings.TrimSpace(req.Message)
|
||||
rawAt := strings.TrimSpace(req.At)
|
||||
if message == "" || rawAt == "" {
|
||||
s.auditClaims(r, "automation.reminder.create", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "at 与 message 均为必填"))
|
||||
return
|
||||
}
|
||||
at, err := time.Parse(time.RFC3339, rawAt)
|
||||
if err != nil {
|
||||
s.auditClaims(r, "automation.reminder.create", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "invalid_at", "at 必须是 RFC3339 时间戳(如 2026-10-05T09:30:00+08:00)"))
|
||||
return
|
||||
}
|
||||
auto, ok := s.automations.(*automations.Service)
|
||||
if !ok {
|
||||
s.auditClaims(r, "automation.reminder.create", "error")
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_unavailable", "自动化服务未配置"))
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(req.ID)
|
||||
if id == "" {
|
||||
id = newReminderID()
|
||||
}
|
||||
rem := automations.Reminder{ID: id, At: at, Message: message}
|
||||
if err := auto.AddReminder(r.Context(), rem); err != nil {
|
||||
s.auditClaims(r, "automation.reminder.create", "error")
|
||||
if errors.Is(err, automations.ErrDuplicate) {
|
||||
writeError(w, apperr.Wrap(apperr.ErrConflict, "already_exists", "提醒 %q 已存在", id))
|
||||
return
|
||||
}
|
||||
if errors.Is(err, automations.ErrInvalidRule) {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "invalid_rule", "提醒 id 或时间非法"))
|
||||
return
|
||||
}
|
||||
writeError(w, apperr.Wrap(apperr.ErrInternal, "automation_failed", "提醒创建失败"))
|
||||
return
|
||||
}
|
||||
s.auditClaims(r, "automation.reminder.create", "ok")
|
||||
writeJSON(w, http.StatusCreated, map[string]any{
|
||||
"id": rem.ID,
|
||||
"at": rem.At.Format(time.RFC3339),
|
||||
"message": rem.Message,
|
||||
"kind": "reminder",
|
||||
})
|
||||
}
|
||||
|
||||
// newReminderID 生成面板省略 id 时使用的提醒 id。它用 crypto/rand 的
|
||||
// 128 位十六进制随机值(与 skills.Store 的 newID 同范式),而不是
|
||||
// UnixNano:Windows 的 time.Now 粒度可粗到毫秒,同一刻度内连创两条
|
||||
// 会撞 id,被 ReminderStore 判为 ErrDuplicate(假失败)。
|
||||
func newReminderID() string {
|
||||
var b [16]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
// crypto/rand 实际不会失败;兜底也在前缀上与随机形式同形。
|
||||
return "rem-" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
}
|
||||
return "rem-" + hex.EncodeToString(b[:])
|
||||
}
|
||||
Reference in new issue
Block a user