Files
Cyrene_v2/pkg/plugins/sdk/panelwhen.go
T
AskaEth c9144c1096
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(plugins): 插件「管理面板声明」库——声明模型 + 闭集校验 + catalog(design/09 §5.2/§5.2.1)
纯库、零接线:不碰 gateway/frontend/Docs/.env*(本片刻意不接线,避免与在途切片争用文件面)。

- pkg/plugins/sdk:面板声明类型(JSON tag 与 design/09 §5.2 示例逐字同形);
  字段类型与 section 类型都是**闭集**;When 三态 + **严格 UnmarshalJSON**(未知条件键报错,
  不静默丢)
- internal/capability/paneldecl:闭集校验(47 条拒绝用例,断言的是**分类哨兵 + JSON 路径**,
  不是错误字符串)· catalog(并发安全、返回拷贝、按 (group, order, 登记序号) 的**全序**稳定排序)·
  when 求值(上下文由 Resolver 注入,本包不读环境变量/文件)· 角色过滤(viewer 只留
  status/log/table 且摘掉 rowActions,纯写面板整体隐藏;未知角色 fail-closed)
- 拒绝粒度:插件 id 非法 ⇒ 整插件拒绝;单个面板非法 ⇒ 只拒该面板 + 聚合显式报错(理由见报告)

【我的验收】隔离 worktree(19 文件)build/vet/linux/darwin 全过、69 包全绿;
我亲手打红变异:把同组同序的回退比较  反成  ⇒ TestCatalog_StableOrder FAIL。
(记录一条观察:比较器已靠 seq 构成全序,故 sort.SliceStable 的稳定性是冗余保护 ——
我第一次用「SliceStable→Slice」打不红,正是因为全序与算法无关;这不是测试虚护。)
2026-10-06 08:06:20 +08:00

79 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package sdk
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"sort"
)
// 本文件定义面板入口的**条件显示**声明 `when`(design/09 §5.2.1 nav.item:
// 「`when` 不满足 ⇒ 条目不出现」,不是灰掉)。
const (
// whenKeyEnabled 是本期唯一支持的条件键:§5.2 的 `"when": { "enabled": true }`
// 是设计里出现过的唯一条件形态,§5.6 的验收判据也只有「未启用的插件 ⇒ 入口不出现」。
// 刻意不留第二个键 —— 闭集外的条件属于「先改设计再加」的范畴。
whenKeyEnabled = "enabled"
)
var (
// ErrInvalidWhen 报告 when 不是合法的条件对象(非对象、null、或 enabled 不是布尔)。
ErrInvalidWhen = errors.New("sdk: invalid when clause")
// ErrUnknownWhenKey 报告 when 里出现了闭集外的条件键。
//
// 刻意**报错而不是忽略**:静默忽略会让插件作者以为条件生效了,而入口照常出现 ——
// §5.2.1 的口径是「绝不静默丢弃」。这条只能在解码期拦住(结构体解码本身会丢弃未知
// 键),所以这里自己实现 UnmarshalJSON。
ErrUnknownWhenKey = errors.New("sdk: unknown when key")
)
// When 是面板入口的条件显示声明。零值语义是「没有条件」,见 paneldecl.Eval。
type When struct {
// Enabled 有三个状态,用指针而不是 bool 表达:
// - nil:没声明这个条件(含整份 when 缺省的 nil 指针)⇒ 不参与判定;
// - &true:要求插件当前已启用(§5.6.1「未启用 ⇒ 入口不出现」);
// - &false:要求插件当前**未**启用 —— 条件是「要求值相等」,写 false 不等于没写。
Enabled *bool `json:"enabled,omitempty"`
}
// UnmarshalJSON 严格解码 when:只接受对象,只接受闭集内的键,enabled 必须是布尔。
//
// 为什么不用默认的 struct 解码:默认解码会**静默丢弃**未知键(`{"enabled":true,"x":1}`
// 会解码成一个看似正常的 When),插件作者会以为自己写了条件却没有任何效果。这里把
// 未知键变成显式拒绝,由插件管理器按 §5.2.1「该面板不注册 + 显式报错」处理。
func (w *When) UnmarshalJSON(raw []byte) error {
trimmed := bytes.TrimSpace(raw)
if bytes.Equal(trimmed, []byte("null")) {
return fmt.Errorf("%w: null is not a condition set", ErrInvalidWhen)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(trimmed, &fields); err != nil {
return fmt.Errorf("%w: %w", ErrInvalidWhen, err)
}
// 按键名排序后再判定:多个未知键时错误信息必须确定(否则同一个坏声明在不同进程里
// 报不同的键,排障与测试都会被搞乱)。
keys := make([]string, 0, len(fields))
for k := range fields {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if k != whenKeyEnabled {
return fmt.Errorf("%w: %q", ErrUnknownWhenKey, k)
}
}
rawEnabled, ok := fields[whenKeyEnabled]
if !ok {
w.Enabled = nil
return nil
}
var enabled bool
if err := json.Unmarshal(rawEnabled, &enabled); err != nil {
return fmt.Errorf("%w: enabled must be a boolean: %w", ErrInvalidWhen, err)
}
w.Enabled = &enabled
return nil
}