feat: harden loose-offline sync for user JWT, schema, and console ops

Enable Binding-scoped agent push/pull, empty-table schema ensure, SyncPage inspect/drop-table, default module import, and agent-bound publish docs from the 宇恒联调意见.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-05 09:47:35 +08:00
parent 76cdcd760e
commit b04b180d30
59 changed files with 4762 additions and 308 deletions

View File

@@ -5,16 +5,20 @@ import (
"strings"
)
// MergeResult describes what was added when merging incoming into base.
// MergeResult describes what was added/updated when merging incoming into base.
type MergeResult struct {
AddedPages []string
AddedEntities []string
AddedResources []string
AddedPages []string
AddedEntities []string
AddedResources []string
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.
// Pages with the same id or route are rejected; new pages 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")
@@ -49,7 +53,11 @@ func MergeInto(base, incoming *Blueprint) (*MergeResult, error) {
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)
@@ -57,22 +65,27 @@ func MergeInto(base, incoming *Blueprint) (*MergeResult, error) {
res.AddedResources = append(res.AddedResources, k)
}
pageByID := map[string]struct{}{}
routeBy := map[string]struct{}{}
for _, p := range base.Pages {
pageByID[p.ID] = struct{}{}
routeBy[p.Route] = struct{}{}
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 _, ok := pageByID[p.ID]; ok {
return nil, fmt.Errorf("page id already exists: %s (use a new page id when adding to an existing app)", p.ID)
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 _, ok := routeBy[p.Route]; ok {
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] = struct{}{}
routeBy[p.Route] = struct{}{}
pageByID[p.ID] = len(base.Pages) - 1
routeBy[p.Route] = p.ID
res.AddedPages = append(res.AddedPages, p.ID)
}
@@ -89,7 +102,11 @@ func MergeInto(base, incoming *Blueprint) (*MergeResult, error) {
base.Seed = incoming.Seed
}
if len(res.AddedPages) == 0 && len(res.AddedEntities) == 0 && len(res.AddedResources) == 0 {
if len(res.AddedPages) == 0 &&
len(res.AddedEntities) == 0 &&
len(res.AddedResources) == 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
@@ -123,6 +140,34 @@ func mergeEntity(base, incoming Entity) Entity {
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 {