Files
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

113 lines
3.0 KiB
Go
Raw Permalink 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 paneldecl
import "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
// 本文件是目录的**拷贝语义**:存进来时拷一次、发出去时再拷一次。
//
// 为什么要两次:§5.2.1 的不变量是「`List` 返回的必须是拷贝」——调用方(gateway / 插件管理器)
// 拿着结果做过滤或补字段时,绝不能改到目录内部状态;反向同理,插件管理器注册后继续改自己
// 那份声明,也不能影响目录里的快照。声明是嵌套结构(sections → fields → 指针字段),
// 浅拷贝会把内部切片暴露出去,所以这里逐层深拷。
// clonePanel 深拷一份面板声明。
func clonePanel(p sdk.Panel) sdk.Panel {
out := p
out.When = cloneWhen(p.When)
if p.Sections != nil {
out.Sections = make([]sdk.Section, len(p.Sections))
for i, s := range p.Sections {
out.Sections[i] = cloneSection(s)
}
}
return out
}
// cloneSection 深拷一个区块(切片与指针字段都要换新)。
func cloneSection(s sdk.Section) sdk.Section {
out := s
if s.Fields != nil {
out.Fields = make([]sdk.Field, len(s.Fields))
for i, f := range s.Fields {
out.Fields[i] = cloneField(f)
}
}
if s.Columns != nil {
out.Columns = make([]sdk.Column, len(s.Columns))
copy(out.Columns, s.Columns)
}
if s.RowActions != nil {
out.RowActions = make([]sdk.RowAction, len(s.RowActions))
copy(out.RowActions, s.RowActions)
}
if s.Submit != nil {
submit := *s.Submit
out.Submit = &submit
}
if s.Source != nil {
source := *s.Source
out.Source = &source
}
return out
}
// cloneField 深拷一个字段(Default 是 any,可能装着 map/slice)。
func cloneField(f sdk.Field) sdk.Field {
out := f
out.Default = cloneValue(f.Default)
if f.Min != nil {
min := *f.Min
out.Min = &min
}
if f.Max != nil {
max := *f.Max
out.Max = &max
}
return out
}
// cloneWhen 深拷 when(Enabled 是指针,共享指针等于共享可变状态)。
func cloneWhen(w *sdk.When) *sdk.When {
if w == nil {
return nil
}
out := &sdk.When{}
if w.Enabled != nil {
enabled := *w.Enabled
out.Enabled = &enabled
}
return out
}
// cloneValue 递归拷贝默认值里的容器。标量(字符串/数字/布尔)本身不可变,直接返回。
//
// 未覆盖的类型(声明由 Go 代码直接构造时可能塞进任意类型)按原值返回并在文档里写明:
// 本包只承诺「结构声明不可被调用方改坏」,不承诺替调用方深拷它自己塞进来的自定义类型。
func cloneValue(v any) any {
switch val := v.(type) {
case map[string]any:
out := make(map[string]any, len(val))
for k, item := range val {
out[k] = cloneValue(item)
}
return out
case map[string]string:
out := make(map[string]string, len(val))
for k, item := range val {
out[k] = item
}
return out
case []any:
out := make([]any, len(val))
for i, item := range val {
out[i] = cloneValue(item)
}
return out
case []string:
out := make([]string, len(val))
copy(out, val)
return out
default:
return v
}
}