纯库、零接线:不碰 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」打不红,正是因为全序与算法无关;这不是测试虚护。)
154 lines
8.2 KiB
Go
154 lines
8.2 KiB
Go
package sdk
|
||
|
||
// 本文件是 design/09-插件生态 §5.2「声明形状(唯一权威形态)」在 Go 侧的唯一定义:
|
||
// 插件作者写下的就是这份形状(plugins/<pluginId>/manifest.json 的 adminPanels 段)。
|
||
//
|
||
// 为什么声明放在 SDK 而不是后端内部包:§5.2.1 的红线是「声明而非编码」—— 插件不注入
|
||
// 任何 JS/CSS/HTML,只说「我要什么字段、怎么校验、失败怎么说」,渲染与流程由宿主负责
|
||
// (面板可接入点 + 插件管理器)。声明既然是插件作者可见的对外契约,它就属于 SDK;
|
||
// JSON tag 与 §5.2 的示例逐字同形,改 tag 等于改对外契约。
|
||
//
|
||
// 本文件只放**数据形状 + 闭集判定**:校验、目录(catalog)、when 求值、角色过滤都在
|
||
// internal/capability/paneldecl —— 数据与策略分开,插件包不依赖任何内部包。
|
||
|
||
// GroupID 是侧边栏折叠分组 id(§5.2 的 `group`)。
|
||
type GroupID string
|
||
|
||
const (
|
||
// GroupPlugins 是本期的唯一分组。依据 §5.2「group: 侧边栏折叠分组 id(本期固定
|
||
// plugins)」—— 闭集只有一个成员,因此空值可以安全地回落到它(回落动作在
|
||
// paneldecl.Register/Validate 里做,SDK 这里只给判定)。
|
||
GroupPlugins GroupID = "plugins"
|
||
)
|
||
|
||
// Valid 报告 g 是否属于本期已知的分组闭集。
|
||
func (g GroupID) Valid() bool { return g == GroupPlugins }
|
||
|
||
// SectionType 是 section 的渲染形状(§5.2 的 section 类型闭集)。
|
||
//
|
||
// 闭集是刻意的:它让「插件声明 UI」在没有任意代码注入的前提下仍然够用,并且每个形状
|
||
// 都能复用既有 Miuix 组件(§5.2 末尾的口径)。
|
||
type SectionType string
|
||
|
||
const (
|
||
// SectionForm 是表单区块:消费 `fields[]` + `submit.action`(§5.2.1 section.form),
|
||
// 由既有字段渲染器泛化渲染。它是四种区块里**唯一含写面**的一种。
|
||
SectionForm SectionType = "form"
|
||
// SectionTable 是表格区块:消费 `columns[]` / `rowActions[]` / `source`(§5.2.1
|
||
// section.table),由既有虚拟滚动表格渲染。
|
||
SectionTable SectionType = "table"
|
||
// SectionStatus 是只读事实面(如「当前生效的降级链」):消费只读 `fields[]`
|
||
// (§5.2.1 section.status)。
|
||
SectionStatus SectionType = "status"
|
||
// SectionLog 是只读日志卡:消费 `source` / `limit`,最新在上(§5.2.1 section.log,
|
||
// 与 L1 口径一致)。
|
||
SectionLog SectionType = "log"
|
||
)
|
||
|
||
// Valid 报告 t 是否属于 section 类型闭集。
|
||
func (t SectionType) Valid() bool {
|
||
switch t {
|
||
case SectionForm, SectionTable, SectionStatus, SectionLog:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ReadOnly 报告该 section 类型本身是否只读。角色过滤的第一道判据
|
||
// (design/09 §5.4.1「只读事实面对 viewer 可见,写动作一律 owner/admin」):
|
||
// 只有 form 自带写面(submit),table 的行内动作需要额外摘除(见 paneldecl.filterPanel)。
|
||
func (t SectionType) ReadOnly() bool { return t != SectionForm }
|
||
|
||
// Panel 是一个插件声明的管理页面(§5.2 的 adminPanels 元素)。
|
||
type Panel struct {
|
||
// ID 是面板 id。它与插件 id 组合成前端路由 /admin/plugin/<pluginId>/<panelId>
|
||
// (§5.7 C),所以必须能直接当 URL 路径段与端点段用 —— 字符集由 paneldecl 校验,
|
||
// 不在这里放宽。
|
||
ID string `json:"id"`
|
||
// Title 是页面标题,同时是侧边栏条目文案(§5.2.1 nav.item 消费 id/title/icon/order/when)。
|
||
Title string `json:"title"`
|
||
// Description 是页面描述。§5.2 的示例里没有这一行,但 §5.2.1 的 `page.root` 一行
|
||
// 明确写「消费 title / description」—— 取并集,缺省为空(不一致处记在 PLG1-report.md)。
|
||
Description string `json:"description,omitempty"`
|
||
// Group 是侧边栏折叠分组 id。空值按 §5.2「本期固定 plugins」回落到 GroupPlugins;
|
||
// 非空但不是闭集成员 ⇒ 校验拒绝。
|
||
Group GroupID `json:"group"`
|
||
// Order 是组内排序,小在前;同 order 时按声明顺序(稳定序,前端导航不许因 map 迭代
|
||
// 抖动)。
|
||
Order int `json:"order"`
|
||
// Icon 是宿主图标名(§5.2 的 `icon`,示例为 "search")。宿主按名字取内置图标,
|
||
// 因此这里只接受图标名,不接受 URL / SVG 片段 —— 它是「无任意代码注入」的一部分。
|
||
Icon string `json:"icon,omitempty"`
|
||
// When 是条件显示声明:不满足 ⇒ 入口**不出现**(不是灰掉,§5.2.1 nav.item)。
|
||
// 缺省(nil)表示无条件显示。求值在 paneldecl.Eval,上下文由调用方注入。
|
||
When *When `json:"when,omitempty"`
|
||
// Sections 是页面内的区块列表。允许为空(§5.1「一个插件可声明**零到多个**管理页面」,
|
||
// 面板本身也可以是纯壳),不做「空面板即非法」的收紧。
|
||
Sections []Section `json:"sections"`
|
||
}
|
||
|
||
// Section 是页面内的一个区块(§5.2.1 section.form / section.table / section.status /
|
||
// section.log 四个接入点之一)。
|
||
//
|
||
// 每种类型**消费哪些键**由 §5.2.1 的接入点表定死,paneldecl 据此做形状校验:形状错了
|
||
// 到前端就是「区块在、功能不在」的半渲染(§5.2.1 点名要防的),所以按面板整体拒掉。
|
||
type Section struct {
|
||
// Type 决定这个区块怎么渲染,必须落在闭集内(form|table|status|log)。
|
||
Type SectionType `json:"type"`
|
||
// ID 是区块 id,在面板内唯一:它既是前端渲染 key,也是「哪一段坏了」的定位锚点。
|
||
ID string `json:"id"`
|
||
// Title 是区块标题。block 无标题会让用户看到一段无来由的表单/表格,因此必填。
|
||
Title string `json:"title"`
|
||
// Fields 是字段声明:form(可写)与 status(只读)消费。
|
||
Fields []Field `json:"fields,omitempty"`
|
||
// Submit 是表单提交动作:只有 form 消费(写面的唯一入口)。
|
||
Submit *Submit `json:"submit,omitempty"`
|
||
// Source 是数据来源:table 与 log 消费。它只给**动作名**,不给 URL(§5.4.4)。
|
||
Source *Source `json:"source,omitempty"`
|
||
// Columns 是表格列声明:只有 table 消费。
|
||
Columns []Column `json:"columns,omitempty"`
|
||
// RowActions 是行内动作:只有 table 消费(如「测试连通」「重配凭据」)。
|
||
RowActions []RowAction `json:"rowActions,omitempty"`
|
||
// Limit 是 log 区块只读的最近条数(§5.2.1 section.log 消费 source/limit)。
|
||
// §5.2 的示例里没有这个键 —— 不一致处记在 PLG1-report.md。
|
||
Limit int `json:"limit,omitempty"`
|
||
}
|
||
|
||
// Source 是 table / log 的数据来源。
|
||
//
|
||
// 为什么只有一个 action 字段:§5.4.4 的硬约束是「插件声明的只是**动作名**,不是任意
|
||
// URL ⇒ 不给插件开 SSRF 面」。所以这里既没有 url 也没有 path —— 想塞 URL 的插件会
|
||
// 在校验期被拒(paneldecl)。
|
||
type Source struct {
|
||
// Action 是插件声明的动作名(示例 plugin.listProviders),由宿主端点
|
||
// POST /api/v1/admin/plugins/{id}/actions/{action} 分发。
|
||
Action string `json:"action"`
|
||
}
|
||
|
||
// Submit 是 form 区块的提交动作(§5.2 的 `submit.action`)。
|
||
type Submit struct {
|
||
// Action 的语义与 Source.Action 相同:只有动作名,没有 URL。
|
||
Action string `json:"action"`
|
||
}
|
||
|
||
// Column 是 table 的一列(§5.2 的 `columns` 元素)。
|
||
type Column struct {
|
||
// Key 是行数据里的字段名,也是列的身份:列内唯一,用于渲染与排序。
|
||
Key string `json:"key"`
|
||
// Label 是表头文案。缺省时宿主回落到 Key(Key 是机器可读标识且列内唯一,
|
||
// 有可靠回落 ⇒ 不作为必填)。
|
||
Label string `json:"label,omitempty"`
|
||
}
|
||
|
||
// RowAction 是 table 的行内动作(§5.2 的 `rowActions` 元素,示例「测试连通」/「重配凭据」)。
|
||
type RowAction struct {
|
||
// ID 是动作在行内的身份,必须唯一 —— 同一行里两个同名动作会让用户点错。
|
||
ID string `json:"id"`
|
||
// Label 是按钮文案。**必填且没有可回落的东西**(不像 Column.Label 能回落到 Key):
|
||
// 一个没有文字的按钮就是半渲染。
|
||
Label string `json:"label"`
|
||
// Action 是插件声明的动作名(同样只有名字、没有 URL)。
|
||
Action string `json:"action"`
|
||
}
|