323 lines
12 KiB
Go
323 lines
12 KiB
Go
package capability
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const SetVersion = "canvas-capabilities/v5"
|
|
|
|
type Registry struct {
|
|
descriptors map[string]Descriptor
|
|
// variants 按 baseType+workflowKind 索引,只能通过 ResolveNode 命中,
|
|
// 不会出现在 Resolve/Types 中,因此不会变成可 add_node 的节点类型。
|
|
variants map[string]string
|
|
}
|
|
|
|
func variantKey(baseType, workflowKind string) string { return baseType + "\x00" + workflowKind }
|
|
|
|
func NewRegistry(descriptors []Descriptor) (*Registry, error) {
|
|
items := make(map[string]Descriptor, len(descriptors))
|
|
variants := map[string]string{}
|
|
generationModes := make(map[string]string, len(descriptors))
|
|
for _, descriptor := range descriptors {
|
|
descriptor.Type = normalizeType(descriptor.Type)
|
|
descriptor.Version = strings.TrimSpace(descriptor.Version)
|
|
descriptor.Label = strings.TrimSpace(descriptor.Label)
|
|
descriptor.Purpose = strings.TrimSpace(descriptor.Purpose)
|
|
descriptor.GoodFor = normalizeFields(descriptor.GoodFor)
|
|
descriptor.NotIdealFor = normalizeFields(descriptor.NotIdealFor)
|
|
descriptor.Tradeoffs = normalizeFields(descriptor.Tradeoffs)
|
|
descriptor.Actions = normalizeFields(descriptor.Actions)
|
|
if descriptor.Type == "" || descriptor.Version == "" || descriptor.Label == "" || descriptor.DefaultWidth <= 0 || descriptor.DefaultHeight <= 0 {
|
|
return nil, fmt.Errorf("invalid canvas capability descriptor %q", descriptor.Type)
|
|
}
|
|
inputKind, err := normalizeInputKind(descriptor.InputKind)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
|
|
}
|
|
descriptor.InputKind = inputKind
|
|
generationMode, err := normalizeGenerationMode(descriptor.GenerationMode)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
|
|
}
|
|
descriptor.GenerationMode = generationMode
|
|
if generationMode != "" {
|
|
if existing, exists := generationModes[generationMode]; exists {
|
|
return nil, fmt.Errorf("generation mode %q is registered by both %q and %q", generationMode, existing, descriptor.Type)
|
|
}
|
|
generationModes[generationMode] = descriptor.Type
|
|
}
|
|
projectionKind, err := normalizeProjectionKind(descriptor.ProjectionKind)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
|
|
}
|
|
descriptor.ProjectionKind = projectionKind
|
|
descriptor.ProjectionField = strings.TrimSpace(descriptor.ProjectionField)
|
|
if (descriptor.ProjectionKind == "") != (descriptor.ProjectionField == "") {
|
|
return nil, fmt.Errorf("capability %q must declare projection kind and field together", descriptor.Type)
|
|
}
|
|
if descriptor.ProjectionField != "" {
|
|
if err := validatePatchPath(descriptor.ProjectionField); err != nil {
|
|
return nil, fmt.Errorf("capability %q has invalid projection field: %w", descriptor.Type, err)
|
|
}
|
|
}
|
|
if descriptor.Connection.MaxInputCount < 0 {
|
|
return nil, fmt.Errorf("capability %q has invalid max input count", descriptor.Type)
|
|
}
|
|
accepted, err := normalizeConnectionKinds(descriptor.Connection.AcceptedInputKinds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
|
|
}
|
|
rejected, err := normalizeConnectionKinds(descriptor.Connection.RejectedInputKinds)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
|
|
}
|
|
rejectedSet := make(map[string]struct{}, len(rejected))
|
|
for _, kind := range rejected {
|
|
rejectedSet[kind] = struct{}{}
|
|
}
|
|
for _, kind := range accepted {
|
|
if _, conflict := rejectedSet[kind]; conflict {
|
|
return nil, fmt.Errorf("capability %q accepts and rejects input kind %q", descriptor.Type, kind)
|
|
}
|
|
}
|
|
if descriptor.Connection.CanSource && descriptor.InputKind == "" {
|
|
return nil, fmt.Errorf("capability %q can source without an input kind", descriptor.Type)
|
|
}
|
|
descriptor.Connection.AcceptedInputKinds = accepted
|
|
descriptor.Connection.RejectedInputKinds = rejected
|
|
descriptor.PatchFields, err = normalizePatchFields(descriptor.PatchFields)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("capability %q: %w", descriptor.Type, err)
|
|
}
|
|
if descriptor.CanUpdate != (len(descriptor.PatchFields) > 0) {
|
|
return nil, fmt.Errorf("capability %q must declare both canUpdate and patch fields, or neither", descriptor.Type)
|
|
}
|
|
descriptor.SummaryFields = normalizeFields(descriptor.SummaryFields)
|
|
descriptor.DetailFields = normalizeFields(descriptor.DetailFields)
|
|
if _, exists := items[descriptor.Type]; exists {
|
|
return nil, fmt.Errorf("duplicate canvas capability %q", descriptor.Type)
|
|
}
|
|
if descriptor.Variant != nil {
|
|
variant := &NodeVariant{BaseType: normalizeType(descriptor.Variant.BaseType), WorkflowKind: strings.TrimSpace(descriptor.Variant.WorkflowKind)}
|
|
descriptor.Variant = variant
|
|
if variant.BaseType == "" || variant.WorkflowKind == "" || variant.BaseType == descriptor.Type {
|
|
return nil, fmt.Errorf("capability %q has invalid node variant", descriptor.Type)
|
|
}
|
|
if descriptor.GenerationMode != "" || descriptor.CreateMetadata != nil {
|
|
return nil, fmt.Errorf("capability variant %q cannot be created or generated directly", descriptor.Type)
|
|
}
|
|
key := variantKey(variant.BaseType, variant.WorkflowKind)
|
|
if existing, exists := variants[key]; exists {
|
|
return nil, fmt.Errorf("node variant %s/%s is registered by both %q and %q", variant.BaseType, variant.WorkflowKind, existing, descriptor.Type)
|
|
}
|
|
variants[key] = descriptor.Type
|
|
}
|
|
items[descriptor.Type] = cloneDescriptor(descriptor)
|
|
}
|
|
for key, variantType := range variants {
|
|
base := items[variantType].Variant.BaseType
|
|
if baseDescriptor, ok := items[base]; !ok || baseDescriptor.Variant != nil {
|
|
return nil, fmt.Errorf("capability variant %q extends unknown base type %q", variantType, strings.SplitN(key, "\x00", 2)[0])
|
|
}
|
|
}
|
|
return &Registry{descriptors: items, variants: variants}, nil
|
|
}
|
|
|
|
func (r *Registry) Resolve(nodeType string) (Descriptor, bool) {
|
|
if r == nil {
|
|
return Descriptor{}, false
|
|
}
|
|
d, ok := r.descriptors[normalizeType(nodeType)]
|
|
if !ok || d.Variant != nil {
|
|
return Descriptor{}, false
|
|
}
|
|
d = cloneDescriptor(d)
|
|
return d, ok
|
|
}
|
|
|
|
// ResolveNode 按真实画布节点解析能力:先匹配 type+metadata.workflowKind 变体(如角色卡),
|
|
// 否则回落到底层类型。读取、连线、引用等拿到节点本身的调用方都应走这里。
|
|
func (r *Registry) ResolveNode(nodeType, workflowKind string) (Descriptor, bool) {
|
|
if r == nil {
|
|
return Descriptor{}, false
|
|
}
|
|
if variantType, ok := r.variants[variantKey(normalizeType(nodeType), strings.TrimSpace(workflowKind))]; ok {
|
|
return cloneDescriptor(r.descriptors[variantType]), true
|
|
}
|
|
return r.Resolve(nodeType)
|
|
}
|
|
func (r *Registry) List() []Descriptor {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
out := make([]Descriptor, 0, len(r.descriptors))
|
|
for _, d := range r.descriptors {
|
|
out = append(out, cloneDescriptor(d))
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Type < out[j].Type })
|
|
return out
|
|
}
|
|
|
|
// Types 只返回可直接创建的底层节点类型;变体通过 List 被发现,但不能作为 add_node 的 nodeType。
|
|
func (r *Registry) Types() []string {
|
|
items := r.List()
|
|
out := make([]string, 0, len(items))
|
|
for _, d := range items {
|
|
if d.Variant == nil {
|
|
out = append(out, d.Type)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
func (r *Registry) GenerationModeNames() []string {
|
|
seen := map[string]struct{}{}
|
|
for _, d := range r.List() {
|
|
if d.GenerationMode != "" {
|
|
seen[d.GenerationMode] = struct{}{}
|
|
}
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for mode := range seen {
|
|
out = append(out, mode)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func (r *Registry) SupportsGenerationMode(mode string) bool {
|
|
_, ok := r.ResolveGenerationMode(mode)
|
|
return ok
|
|
}
|
|
|
|
func (r *Registry) ResolveGenerationMode(mode string) (Descriptor, bool) {
|
|
mode = normalizeType(mode)
|
|
if mode == "" || r == nil {
|
|
return Descriptor{}, false
|
|
}
|
|
for _, descriptor := range r.descriptors {
|
|
if descriptor.GenerationMode == mode {
|
|
return cloneDescriptor(descriptor), true
|
|
}
|
|
}
|
|
return Descriptor{}, false
|
|
}
|
|
func (r *Registry) Hash() string {
|
|
data, _ := json.Marshal(registryHashItems(r.List()))
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func normalizeFields(fields []string) []string {
|
|
seen := make(map[string]struct{}, len(fields))
|
|
out := make([]string, 0, len(fields))
|
|
for _, field := range fields {
|
|
field = strings.TrimSpace(field)
|
|
if field == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[field]; exists {
|
|
continue
|
|
}
|
|
seen[field] = struct{}{}
|
|
out = append(out, field)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func cloneDescriptor(descriptor Descriptor) Descriptor {
|
|
descriptor.Connection.AcceptedInputKinds = append([]string(nil), descriptor.Connection.AcceptedInputKinds...)
|
|
descriptor.Connection.RejectedInputKinds = append([]string(nil), descriptor.Connection.RejectedInputKinds...)
|
|
descriptor.GoodFor = append([]string(nil), descriptor.GoodFor...)
|
|
descriptor.NotIdealFor = append([]string(nil), descriptor.NotIdealFor...)
|
|
descriptor.Tradeoffs = append([]string(nil), descriptor.Tradeoffs...)
|
|
descriptor.Actions = append([]string(nil), descriptor.Actions...)
|
|
descriptor.SummaryFields = append([]string(nil), descriptor.SummaryFields...)
|
|
descriptor.DetailFields = append([]string(nil), descriptor.DetailFields...)
|
|
patchFields := make(map[string]PatchField, len(descriptor.PatchFields))
|
|
for key, field := range descriptor.PatchFields {
|
|
patchFields[key] = field
|
|
}
|
|
descriptor.PatchFields = patchFields
|
|
if descriptor.Variant != nil {
|
|
variant := *descriptor.Variant
|
|
descriptor.Variant = &variant
|
|
}
|
|
return descriptor
|
|
}
|
|
|
|
type hashDescriptor struct {
|
|
Type string
|
|
Version string
|
|
Label string
|
|
Purpose string
|
|
GoodFor []string
|
|
NotIdealFor []string
|
|
Tradeoffs []string
|
|
Actions []string
|
|
DefaultWidth float64
|
|
DefaultHeight float64
|
|
InputKind string
|
|
GenerationMode string
|
|
Connection hashConnectionPolicy
|
|
CanUpdate bool
|
|
SummaryFields []string
|
|
DetailFields []string
|
|
ProjectionKind string
|
|
ProjectionField string
|
|
PatchFields []hashPatchField
|
|
Variant *NodeVariant
|
|
}
|
|
|
|
type hashConnectionPolicy struct {
|
|
CanSource bool
|
|
CanTarget bool
|
|
CanReference bool
|
|
AcceptedInputKinds []string
|
|
RejectedInputKinds []string
|
|
MaxInputCount int
|
|
}
|
|
|
|
type hashPatchField struct {
|
|
Key string
|
|
Path string
|
|
Kind string
|
|
Label string
|
|
Order int
|
|
Description string
|
|
MaxRunes int
|
|
}
|
|
|
|
func registryHashItems(descriptors []Descriptor) []hashDescriptor {
|
|
items := make([]hashDescriptor, 0, len(descriptors))
|
|
for _, descriptor := range descriptors {
|
|
keys := make([]string, 0, len(descriptor.PatchFields))
|
|
for key := range descriptor.PatchFields {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
patchFields := make([]hashPatchField, 0, len(keys))
|
|
for _, key := range keys {
|
|
field := descriptor.PatchFields[key]
|
|
patchFields = append(patchFields, hashPatchField{Key: key, Path: field.Path, Kind: field.Kind, Label: field.Label, Order: field.Order, Description: field.Description, MaxRunes: field.MaxRunes})
|
|
}
|
|
items = append(items, hashDescriptor{
|
|
Type: descriptor.Type, Version: descriptor.Version, Label: descriptor.Label,
|
|
Purpose: descriptor.Purpose, GoodFor: descriptor.GoodFor, NotIdealFor: descriptor.NotIdealFor,
|
|
Tradeoffs: descriptor.Tradeoffs, Actions: descriptor.Actions,
|
|
DefaultWidth: descriptor.DefaultWidth, DefaultHeight: descriptor.DefaultHeight,
|
|
InputKind: descriptor.InputKind, GenerationMode: descriptor.GenerationMode,
|
|
ProjectionKind: descriptor.ProjectionKind, ProjectionField: descriptor.ProjectionField,
|
|
Connection: hashConnectionPolicy{CanSource: descriptor.Connection.CanSource, CanTarget: descriptor.Connection.CanTarget, CanReference: descriptor.Connection.CanReference, AcceptedInputKinds: descriptor.Connection.AcceptedInputKinds, RejectedInputKinds: descriptor.Connection.RejectedInputKinds, MaxInputCount: descriptor.Connection.MaxInputCount},
|
|
CanUpdate: descriptor.CanUpdate, SummaryFields: descriptor.SummaryFields, DetailFields: descriptor.DetailFields, PatchFields: patchFields,
|
|
Variant: descriptor.Variant,
|
|
})
|
|
}
|
|
return items
|
|
}
|