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

@@ -0,0 +1,165 @@
package blueprint
import (
"encoding/json"
"strings"
)
// defaultImportExportOps Z9业务表默认允许导入/导出。
var defaultImportExportOps = []string{"import", "export"}
// defaultListActions Z9列表页默认工具栏含导入
var defaultListActions = []string{"create", "edit", "delete", "export", "import", "refresh"}
// EnsureDefaultImportExport 发布/合并兜底Z9d
// 可写业务 resource 缺 import/export 则补上list 页 actions 缺 import 则补上。
// 跳过:只读 resource仅 list/getmeta.ui.disable_default_import == true。
func (bp *Blueprint) EnsureDefaultImportExport() {
if bp == nil {
return
}
if disableDefaultImport(bp) {
return
}
for i := range bp.Apis.Resources {
r := &bp.Apis.Resources[i]
if resourceReadOnly(r.Operations) {
continue
}
r.Operations = ensureOpList(r.Operations, defaultImportExportOps...)
}
for i := range bp.Pages {
p := &bp.Pages[i]
if !strings.EqualFold(strings.TrimSpace(p.Type), "list") {
continue
}
if p.Layout == nil {
p.Layout = &PageLayout{}
}
p.Layout.Actions = ensureListActions(p.Layout.Actions)
if p.Layout.ActionLabels == nil {
p.Layout.ActionLabels = map[string]string{}
}
if _, ok := p.Layout.ActionLabels["import"]; !ok {
p.Layout.ActionLabels["import"] = "导入 Excel"
}
if _, ok := p.Layout.ActionLabels["export"]; !ok {
p.Layout.ActionLabels["export"] = "导出 Excel"
}
}
}
func disableDefaultImport(bp *Blueprint) bool {
if len(bp.Meta.UI) == 0 {
return false
}
var ui map[string]any
if err := json.Unmarshal(bp.Meta.UI, &ui); err != nil {
return false
}
v, ok := ui["disable_default_import"]
if !ok {
return false
}
switch t := v.(type) {
case bool:
return t
case string:
return strings.EqualFold(t, "true") || t == "1"
default:
return false
}
}
func resourceReadOnly(ops []string) bool {
if len(ops) == 0 {
return false
}
writable := false
for _, op := range ops {
switch strings.ToLower(strings.TrimSpace(op)) {
case "create", "update", "delete", "import", "export":
writable = true
}
}
// 仅 list/get → 只读,不强加 import
if !writable {
onlyRead := true
for _, op := range ops {
switch strings.ToLower(strings.TrimSpace(op)) {
case "list", "get", "":
default:
onlyRead = false
}
}
return onlyRead
}
return false
}
func ensureOpList(ops []string, add ...string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(ops)+len(add))
for _, op := range ops {
op = strings.ToLower(strings.TrimSpace(op))
if op == "" {
continue
}
if _, ok := seen[op]; ok {
continue
}
seen[op] = struct{}{}
out = append(out, op)
}
for _, op := range add {
op = strings.ToLower(strings.TrimSpace(op))
if op == "" {
continue
}
if _, ok := seen[op]; ok {
continue
}
seen[op] = struct{}{}
out = append(out, op)
}
return out
}
func ensureListActions(actions []string) []string {
if len(actions) == 0 {
return append([]string{}, defaultListActions...)
}
seen := map[string]struct{}{}
out := make([]string, 0, len(actions)+2)
for _, a := range actions {
a = strings.ToLower(strings.TrimSpace(a))
if a == "" {
continue
}
if _, ok := seen[a]; ok {
continue
}
seen[a] = struct{}{}
out = append(out, a)
}
for _, need := range []string{"import", "export"} {
if _, ok := seen[need]; ok {
continue
}
// 插在 refresh 前;若无 refresh 则追加
inserted := false
for i, a := range out {
if a == "refresh" {
out = append(out[:i], append([]string{need}, out[i:]...)...)
seen[need] = struct{}{}
inserted = true
break
}
}
if !inserted {
out = append(out, need)
seen[need] = struct{}{}
}
}
return out
}