Files
ai_site/platform/internal/blueprint/merge.go
whm cb56e6847e feat: add Z12/Z13 bind APIs, stock import, and sync docs
Enable auto default sync channels on agent activate, bind-code/phone confirm flows, publish ALTER, and align admin/yuheng docs with the production bind path.
2026-08-05 11:47:20 +08:00

217 lines
6.0 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 blueprint
import (
"fmt"
"strings"
)
// MergeResult describes what was added/updated when merging incoming into base.
type MergeResult struct {
AddedPages []string
AddedEntities []string
AddedResources []string
AddedFields []string // entity.field
UpdatedPages []string
UpdatedResources []string
}
// MergeInto merges incoming pages/entities/apis into base (existing published app).
// Existing entities keep their fields; new fields on known entities are appended.
// Same-path resources union operations (e.g. add import).
// Same page id合并 actions不再报错支持「只改 API/按钮、不新建页」的编辑发布)。
// 仅 route 冲突且 id 不同仍拒绝。
func MergeInto(base, incoming *Blueprint) (*MergeResult, error) {
if base == nil {
return nil, fmt.Errorf("base blueprint is nil")
}
if incoming == nil {
return nil, fmt.Errorf("incoming blueprint is nil")
}
res := &MergeResult{}
entityByName := map[string]int{}
for i, e := range base.Entities {
entityByName[e.Name] = i
}
for _, e := range incoming.Entities {
if idx, ok := entityByName[e.Name]; ok {
before := len(base.Entities[idx].Fields)
base.Entities[idx] = mergeEntity(base.Entities[idx], e)
if len(base.Entities[idx].Fields) > before {
for _, f := range base.Entities[idx].Fields[before:] {
res.AddedFields = append(res.AddedFields, e.Name+"."+f.Name)
}
}
continue
}
base.Entities = append(base.Entities, e)
entityByName[e.Name] = len(base.Entities) - 1
res.AddedEntities = append(res.AddedEntities, e.Name)
}
pathKey := func(p string) string {
p = strings.TrimPrefix(strings.TrimSpace(p), "/")
return p
}
resByPath := map[string]int{}
for i, r := range base.Apis.Resources {
resByPath[pathKey(r.Path)] = i
}
for _, r := range incoming.Apis.Resources {
k := pathKey(r.Path)
if idx, ok := resByPath[k]; ok {
before := len(base.Apis.Resources[idx].Operations)
base.Apis.Resources[idx] = mergeResource(base.Apis.Resources[idx], r)
if len(base.Apis.Resources[idx].Operations) > before {
res.UpdatedResources = append(res.UpdatedResources, k)
}
continue
}
base.Apis.Resources = append(base.Apis.Resources, r)
resByPath[k] = len(base.Apis.Resources) - 1
res.AddedResources = append(res.AddedResources, k)
}
pageByID := map[string]int{}
routeBy := map[string]string{} // route -> page id
for i, p := range base.Pages {
pageByID[p.ID] = i
routeBy[p.Route] = p.ID
}
for _, p := range incoming.Pages {
if idx, ok := pageByID[p.ID]; ok {
before := pageActionCount(base.Pages[idx])
base.Pages[idx] = mergePage(base.Pages[idx], p)
if pageActionCount(base.Pages[idx]) > before {
res.UpdatedPages = append(res.UpdatedPages, p.ID)
}
continue
}
if owner, ok := routeBy[p.Route]; ok && owner != p.ID {
return nil, fmt.Errorf("page route already exists: %s", p.Route)
}
base.Pages = append(base.Pages, p)
pageByID[p.ID] = len(base.Pages) - 1
routeBy[p.Route] = p.ID
res.AddedPages = append(res.AddedPages, p.ID)
}
if incoming.Meta.Description != "" && base.Meta.Description == "" {
base.Meta.Description = incoming.Meta.Description
}
if incoming.Meta.UIPreset != "" && base.Meta.UIPreset == "" {
base.Meta.UIPreset = incoming.Meta.UIPreset
}
if len(incoming.Meta.UI) > 0 && len(base.Meta.UI) == 0 {
base.Meta.UI = incoming.Meta.UI
}
if incoming.Seed != nil && base.Seed == nil {
base.Seed = incoming.Seed
}
if len(res.AddedPages) == 0 &&
len(res.AddedEntities) == 0 &&
len(res.AddedResources) == 0 &&
len(res.AddedFields) == 0 &&
len(res.UpdatedPages) == 0 &&
len(res.UpdatedResources) == 0 {
return nil, fmt.Errorf("nothing new to publish: provide newly generated pages (and entities/apis if needed) for an existing app")
}
return res, nil
}
func mergeEntity(base, incoming Entity) Entity {
fieldBy := map[string]struct{}{}
for _, f := range base.Fields {
fieldBy[f.Name] = struct{}{}
}
for _, f := range incoming.Fields {
if _, ok := fieldBy[f.Name]; ok {
continue
}
base.Fields = append(base.Fields, f)
fieldBy[f.Name] = struct{}{}
}
idxBy := map[string]struct{}{}
for _, ix := range base.Indexes {
idxBy[ix.Name] = struct{}{}
}
for _, ix := range incoming.Indexes {
if _, ok := idxBy[ix.Name]; ok {
continue
}
base.Indexes = append(base.Indexes, ix)
}
if base.Label == "" && incoming.Label != "" {
base.Label = incoming.Label
}
return base
}
func pageActionCount(p Page) int {
if p.Layout == nil {
return 0
}
return len(p.Layout.Actions)
}
func mergePage(base, incoming Page) Page {
if incoming.Layout == nil || len(incoming.Layout.Actions) == 0 {
return base
}
if base.Layout == nil {
base.Layout = &PageLayout{}
}
actSet := map[string]struct{}{}
for _, a := range base.Layout.Actions {
actSet[a] = struct{}{}
}
for _, a := range incoming.Layout.Actions {
if _, ok := actSet[a]; ok {
continue
}
base.Layout.Actions = append(base.Layout.Actions, a)
actSet[a] = struct{}{}
}
return base
}
func mergeResource(base, incoming APIResource) APIResource {
opSet := map[string]struct{}{}
for _, op := range base.Operations {
opSet[op] = struct{}{}
}
for _, op := range incoming.Operations {
if _, ok := opSet[op]; ok {
continue
}
base.Operations = append(base.Operations, op)
opSet[op] = struct{}{}
}
if base.List == nil && incoming.List != nil {
base.List = incoming.List
} else if base.List != nil && incoming.List != nil {
filt := map[string]struct{}{}
for _, f := range base.List.AllowedFilters {
filt[f] = struct{}{}
}
for _, f := range incoming.List.AllowedFilters {
if _, ok := filt[f]; !ok {
base.List.AllowedFilters = append(base.List.AllowedFilters, f)
filt[f] = struct{}{}
}
}
sorts := map[string]struct{}{}
for _, s := range base.List.AllowedSorts {
sorts[s] = struct{}{}
}
for _, s := range incoming.List.AllowedSorts {
if _, ok := sorts[s]; !ok {
base.List.AllowedSorts = append(base.List.AllowedSorts, s)
sorts[s] = struct{}{}
}
}
}
return base
}