feat: Z36 host_meta-only publish + Z37 SyncPage LWW UX

Allow display-name-only publish via host_meta; stop treating company conflicts 403 as a fault and ship rebuilt web dist.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-07 12:01:18 +08:00
parent 4b6c90904b
commit 4f9087bd38
9 changed files with 597 additions and 387 deletions

View File

@@ -229,6 +229,9 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
bp.Apis.BasePath = "/api/v1/apps/" + slug
mergeRes, err = blueprint.MergeInto(bp, incoming)
if err != nil {
if isNothingNewToPublish(err) {
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
}
return nil, err
}
publishMode = "pages_added"
@@ -249,6 +252,9 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
bp.Apis.BasePath = "/api/v1/apps/" + slug
mergeRes, err = blueprint.MergeInto(bp, incoming)
if err != nil {
if isNothingNewToPublish(err) {
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
}
return nil, err
}
publishMode = "pages_added"
@@ -258,6 +264,13 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
}
}
// Z36有蓝图增量时也把 host_meta.module_name 落到蓝图/模块显示名
if req.HostMeta != nil {
if n := strings.TrimSpace(req.HostMeta.ModuleName); n != "" {
bp.Meta.Name = n
}
}
bp.EnsureDefaultImportExport()
if err := bp.Validate(slug); err != nil {
return nil, err
@@ -483,6 +496,112 @@ func (l *PublishLogic) audit(action, detail string) error {
return l.svcCtx.Audit.Log(l.ctx, authx.TenantID(l.ctx), authx.UserID(l.ctx), action, detail)
}
func isNothingNewToPublish(err error) bool {
return err != nil && strings.Contains(err.Error(), "nothing new to publish")
}
// handleNoBlueprintDelta Z36merge 无增量时,有 module_name 则仅更新 host_meta否则中文业务错误。
func (l *PublishLogic) handleNoBlueprintDelta(
existing *meta.AppRecord,
req *types.PublishReq,
slug string,
tenantID, userID int64,
) (*types.PublishResp, error) {
name := ""
if req.HostMeta != nil {
name = strings.TrimSpace(req.HostMeta.ModuleName)
}
if name == "" {
return nil, fmt.Errorf("没有可发布的蓝图增量(页面/实体/接口未变化)。若仅修改模块显示名,请在 host_meta.module_name 传入新名称后重试 [NO_BLUEPRINT_DELTA]")
}
return l.publishHostMetaOnly(existing, req, slug, tenantID, userID, name)
}
// publishHostMetaOnly 无蓝图增量时只更新模块显示名并重签胶囊,不跑 DDL。
func (l *PublishLogic) publishHostMetaOnly(
existing *meta.AppRecord,
req *types.PublishReq,
slug string,
tenantID, userID int64,
moduleName string,
) (*types.PublishResp, error) {
if existing == nil || existing.Blueprint == nil {
return nil, fmt.Errorf("existing app has no blueprint")
}
bp := existing.Blueprint
bp.Meta.Name = moduleName
bp.Meta.Slug = slug
if bp.Apis.BasePath == "" {
bp.Apis.BasePath = "/api/v1/apps/" + slug
}
rec := existing
rec.Name = moduleName
rec.Blueprint = bp
rec.UpdatedAt = time.Now().UTC()
if rec.Status == "" {
rec.Status = meta.StatusPublished
}
if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
return nil, fmt.Errorf("meta save: %w", err)
}
_ = l.audit("publish.host_meta", audit.DetailJSON(map[string]any{
"slug": slug, "module_name": moduleName, "user_id": userID, "publish_mode": "host_meta_updated",
}))
ownerID := authx.AgentID(l.ctx)
if ownerID <= 0 {
ownerID = userID
}
secret := l.svcCtx.Config.Agent.CapsuleSecret
if secret == "" {
secret = l.svcCtx.JWT.AccessSecret
}
accessPath := ""
accessURL := ""
if secret != "" && ownerID > 0 {
_, filePath, err := agentcap.SealModulePath(secret, tenantID, ownerID, slug)
if err == nil {
accessPath = filePath
base := strings.TrimRight(l.svcCtx.Config.PublicBaseURL, "/")
if req.HostMeta != nil && strings.TrimSpace(req.HostMeta.HostBaseURL) != "" {
accessURL = strings.TrimRight(strings.TrimSpace(req.HostMeta.HostBaseURL), "/")
} else if base != "" {
accessURL = base + "/api/v1/public/" + filePath + "/blueprint"
}
}
}
publishStyle := ""
if req.HostMeta != nil {
publishStyle = strings.TrimSpace(req.HostMeta.PublishStyle)
}
if publishStyle == "" {
publishStyle = "immediate"
}
endpoints := rec.Endpoints
if len(endpoints) == 0 {
endpoints = buildEndpoints(bp)
}
return &types.PublishResp{
AppID: rec.AppID,
Slug: slug,
SchemaName: rec.SchemaName,
DatabaseName: rec.DatabaseName,
Status: string(rec.Status),
Endpoints: endpoints,
MemoryMode: l.svcCtx.MemoryMode,
PublishMode: "host_meta_updated",
ModuleName: moduleName,
PublishStyle: publishStyle,
AccessPath: accessPath,
AccessURL: accessURL,
PublishedAt: rec.UpdatedAt.UTC().Format(time.RFC3339),
OwnerID: ownerID,
}, nil
}
func buildEndpoints(bp *blueprint.Blueprint) []string {
base := strings.TrimRight(bp.Apis.BasePath, "/")
if base == "" {

View File

@@ -29,7 +29,7 @@ type PublishResp struct {
Endpoints []string `json:"endpoints"`
DDL []string `json:"ddl,omitempty"`
MemoryMode bool `json:"memory_mode"`
PublishMode string `json:"publish_mode"` // created | pages_added | replaced
PublishMode string `json:"publish_mode"` // created | pages_added | replaced | host_meta_updated
AddedPages []string `json:"added_pages,omitempty"`
AddedEntities []string `json:"added_entities,omitempty"`
AddedResources []string `json:"added_resources,omitempty"`