fix: Z39 isolate module data by slug
Keep agent-bound modules in per-app schemas and prevent stale frontend routes from displaying another module's blueprint or rows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,13 +18,15 @@ const Prefix = "AJZ1"
|
||||
|
||||
// Descriptor 智能体解密后才能看到的请求契约(前端不展示明文)。
|
||||
type Descriptor struct {
|
||||
Version string `json:"v"`
|
||||
BaseURL string `json:"base_url"`
|
||||
AppSlug string `json:"app_slug"`
|
||||
TenantHint string `json:"tenant_hint,omitempty"`
|
||||
Auth AuthSpec `json:"auth"`
|
||||
Resources []ResourceSpec `json:"resources"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Version string `json:"v"`
|
||||
BaseURL string `json:"base_url"`
|
||||
AppSlug string `json:"app_slug"`
|
||||
TenantHint string `json:"tenant_hint,omitempty"`
|
||||
SchemaName string `json:"schema_name,omitempty"`
|
||||
BlueprintRevision string `json:"blueprint_revision,omitempty"`
|
||||
Auth AuthSpec `json:"auth"`
|
||||
Resources []ResourceSpec `json:"resources"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type AuthSpec struct {
|
||||
@@ -33,13 +35,14 @@ type AuthSpec struct {
|
||||
}
|
||||
|
||||
type ResourceSpec struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Methods []string `json:"methods"`
|
||||
Filters []string `json:"filters,omitempty"`
|
||||
Sorts []string `json:"sorts,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Methods []string `json:"methods"`
|
||||
Filters []string `json:"filters,omitempty"`
|
||||
Sorts []string `json:"sorts,omitempty"`
|
||||
Fields []FieldSpec `json:"fields,omitempty"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
Table string `json:"table,omitempty"`
|
||||
}
|
||||
|
||||
type FieldSpec struct {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package blueprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -48,12 +49,12 @@ type Storage struct {
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
Name string `json:"name"`
|
||||
Table string `json:"table"`
|
||||
Label string `json:"label"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
Fields []Field `json:"fields"`
|
||||
Indexes []Index `json:"indexes,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Table string `json:"table"`
|
||||
Label string `json:"label"`
|
||||
PrimaryKey string `json:"primary_key"`
|
||||
Fields []Field `json:"fields"`
|
||||
Indexes []Index `json:"indexes,omitempty"`
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
@@ -271,10 +272,7 @@ func (bp *Blueprint) AssignSchemaName(tenantID int64) string {
|
||||
bp.Storage.SchemaName = "public"
|
||||
return "public"
|
||||
}
|
||||
name := fmt.Sprintf("app_t%d_%s", tenantID, bp.Meta.Slug)
|
||||
if len(name) > 48 {
|
||||
name = name[:48]
|
||||
}
|
||||
name := scopedIdent(fmt.Sprintf("app_t%d_%s", tenantID, bp.Meta.Slug))
|
||||
bp.Storage.SchemaName = name
|
||||
return name
|
||||
}
|
||||
@@ -284,11 +282,30 @@ func (bp *Blueprint) AssignDatabaseName(tenantID int64) string {
|
||||
if bp.Storage.Mode != "database_per_app" {
|
||||
return ""
|
||||
}
|
||||
name := fmt.Sprintf("appdb_t%d_%s", tenantID, bp.Meta.Slug)
|
||||
if len(name) > 48 {
|
||||
name = name[:48]
|
||||
return scopedIdent(fmt.Sprintf("appdb_t%d_%s", tenantID, bp.Meta.Slug))
|
||||
}
|
||||
|
||||
// scopedIdent 保留可读前缀,并为超长标识符附加内容哈希,避免不同 slug 截断后碰撞。
|
||||
func scopedIdent(raw string) string {
|
||||
if len(raw) <= 48 {
|
||||
return raw
|
||||
}
|
||||
return name
|
||||
sum := sha256.Sum256([]byte(raw))
|
||||
suffix := fmt.Sprintf("_%x", sum[:6])
|
||||
return raw[:48-len(suffix)] + suffix
|
||||
}
|
||||
|
||||
// Revision 返回蓝图内容的稳定短哈希,用于发布回执、缓存隔离与诊断日志。
|
||||
func (bp *Blueprint) Revision() string {
|
||||
if bp == nil {
|
||||
return ""
|
||||
}
|
||||
raw, err := json.Marshal(bp)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
return fmt.Sprintf("%x", sum[:8])
|
||||
}
|
||||
|
||||
func checkIdent(field, v string) error {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package blueprint
|
||||
package blueprint
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -18,6 +18,32 @@ func TestNormalizeIdent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignSchemaNameKeepsLongSlugsDistinct(t *testing.T) {
|
||||
prefix := "module_with_a_very_long_shared_slug_prefix_that_used_to_collide_"
|
||||
a := &Blueprint{Meta: Meta{Slug: prefix + "a"}, Storage: Storage{Mode: "schema_per_app"}}
|
||||
b := &Blueprint{Meta: Meta{Slug: prefix + "b"}, Storage: Storage{Mode: "schema_per_app"}}
|
||||
|
||||
gotA := a.AssignSchemaName(12)
|
||||
gotB := b.AssignSchemaName(12)
|
||||
if gotA == gotB {
|
||||
t.Fatalf("different slugs resolved to the same schema: %q", gotA)
|
||||
}
|
||||
if len(gotA) > 48 || len(gotB) > 48 {
|
||||
t.Fatalf("schema names exceed identifier limit: %q / %q", gotA, gotB)
|
||||
}
|
||||
if gotA != a.AssignSchemaName(12) {
|
||||
t.Fatalf("schema name is not deterministic: %q", gotA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevisionChangesWithSlug(t *testing.T) {
|
||||
a := &Blueprint{Version: "1.0", Meta: Meta{Slug: "whm1"}}
|
||||
b := &Blueprint{Version: "1.0", Meta: Meta{Slug: "whm11"}}
|
||||
if a.Revision() == "" || a.Revision() == b.Revision() {
|
||||
t.Fatalf("revision must include blueprint slug: %q / %q", a.Revision(), b.Revision())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFields(t *testing.T) {
|
||||
bp := &Blueprint{
|
||||
Version: "1.0",
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aijianzhan/platform/internal/agentcap"
|
||||
"aijianzhan/platform/internal/apidef"
|
||||
"aijianzhan/platform/internal/audit"
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/agentcap"
|
||||
"aijianzhan/platform/internal/logic/applogic"
|
||||
"aijianzhan/platform/internal/meta"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
@@ -1069,6 +1069,9 @@ func publicBlueprintHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
authx.WriteError(w, http.StatusNotFound, "blueprint missing")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-AJZ-App-Slug", app.Slug)
|
||||
w.Header().Set("X-AJZ-Blueprint-Revision", app.Blueprint.Revision())
|
||||
httpx.OkJson(w, app.Blueprint)
|
||||
}
|
||||
}
|
||||
@@ -1122,6 +1125,12 @@ func publicListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
authx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-AJZ-App-Slug", app.Slug)
|
||||
w.Header().Set("X-AJZ-Resolved-Schema", app.SchemaName)
|
||||
if app.Blueprint != nil {
|
||||
w.Header().Set("X-AJZ-Blueprint-Revision", app.Blueprint.Revision())
|
||||
}
|
||||
httpx.OkJson(w, resp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,12 +309,14 @@ func (l *CapsuleLogic) Build(slug string) (*types.CapsuleResp, error) {
|
||||
base = fmt.Sprintf("http://127.0.0.1:%d", l.svcCtx.Config.Port)
|
||||
}
|
||||
desc := &agentcap.Descriptor{
|
||||
Version: "1",
|
||||
BaseURL: strings.TrimRight(base, "/"),
|
||||
AppSlug: slug,
|
||||
TenantHint: fmt.Sprintf("t%d", tenantID),
|
||||
Auth: agentcap.AuthSpec{Type: "bearer_jwt", Header: "Authorization"},
|
||||
Notes: "Decrypt with agent_key from /auth/token. Never expose plaintext API map in UI.",
|
||||
Version: "1",
|
||||
BaseURL: strings.TrimRight(base, "/"),
|
||||
AppSlug: slug,
|
||||
TenantHint: fmt.Sprintf("t%d", tenantID),
|
||||
SchemaName: app.SchemaName,
|
||||
BlueprintRevision: app.Blueprint.Revision(),
|
||||
Auth: agentcap.AuthSpec{Type: "bearer_jwt", Header: "Authorization"},
|
||||
Notes: "Decrypt with agent_key from /auth/token. Never expose plaintext API map in UI.",
|
||||
}
|
||||
for _, r := range app.Blueprint.Apis.Resources {
|
||||
path := r.Path
|
||||
@@ -337,11 +339,13 @@ func (l *CapsuleLogic) Build(slug string) (*types.CapsuleResp, error) {
|
||||
}
|
||||
var entityFields []agentcap.FieldSpec
|
||||
pk := "id"
|
||||
table := ""
|
||||
for _, e := range app.Blueprint.Entities {
|
||||
if e.Name != r.Entity {
|
||||
continue
|
||||
}
|
||||
pk = e.PrimaryKey
|
||||
table = e.Table
|
||||
for _, f := range e.Fields {
|
||||
entityFields = append(entityFields, agentcap.FieldSpec{Name: f.Name, Type: f.Type})
|
||||
}
|
||||
@@ -360,6 +364,7 @@ func (l *CapsuleLogic) Build(slug string) (*types.CapsuleResp, error) {
|
||||
Sorts: sorts,
|
||||
Fields: entityFields,
|
||||
PrimaryKey: pk,
|
||||
Table: app.SchemaName + "." + table,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ package applogic
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"aijianzhan/platform/internal/authx"
|
||||
"aijianzhan/platform/internal/crud"
|
||||
"aijianzhan/platform/internal/meta"
|
||||
"aijianzhan/platform/internal/svc"
|
||||
"aijianzhan/platform/internal/types"
|
||||
)
|
||||
@@ -40,7 +42,7 @@ func (l *CrudLogic) withOrgScope() context.Context {
|
||||
func (l *CrudLogic) List(slug, resource string, page, pageSize int, filters map[string]string, sortBy string) (*types.PageResult, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
ref, err := l.resolveResource(ctx, tenantID, slug, resource, "list")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -60,7 +62,7 @@ func (l *CrudLogic) List(slug, resource string, page, pageSize int, filters map[
|
||||
func (l *CrudLogic) Get(slug, resource, id string) (map[string]any, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
ref, err := l.resolveResource(ctx, tenantID, slug, resource, "get")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -71,7 +73,7 @@ func (l *CrudLogic) Create(slug, resource string, body map[string]any) (map[stri
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
userID := authx.UserID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
ref, err := l.resolveResource(ctx, tenantID, slug, resource, "create")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -81,7 +83,7 @@ func (l *CrudLogic) Create(slug, resource string, body map[string]any) (map[stri
|
||||
func (l *CrudLogic) Update(slug, resource, id string, body map[string]any) (map[string]any, error) {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
ref, err := l.resolveResource(ctx, tenantID, slug, resource, "update")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -91,13 +93,31 @@ func (l *CrudLogic) Update(slug, resource, id string, body map[string]any) (map[
|
||||
func (l *CrudLogic) Delete(slug, resource, id string) error {
|
||||
ctx := l.withOrgScope()
|
||||
tenantID := authx.TenantID(ctx)
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
ref, err := l.resolveResource(ctx, tenantID, slug, resource, "delete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return l.svcCtx.CRUD.Delete(ctx, ref, tenantID, id)
|
||||
}
|
||||
|
||||
func (l *CrudLogic) resolveResource(ctx context.Context, tenantID int64, slug, resource, operation string) (*meta.ResourceRef, error) {
|
||||
ref, err := l.svcCtx.Meta.ResolveResource(ctx, tenantID, slug, resource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ref == nil || ref.App == nil || ref.App.Slug != slug {
|
||||
return nil, fmt.Errorf("resource isolation mismatch for app %s", slug)
|
||||
}
|
||||
if ref.App.SchemaName == "" || ref.Entity.Table == "" {
|
||||
return nil, fmt.Errorf("resource storage mapping missing for app %s", slug)
|
||||
}
|
||||
log.Printf(
|
||||
"app_resource op=%s tenant=%d requested_slug=%s resource=%s resolved_schema=%s resolved_table=%s blueprint_revision=%s",
|
||||
operation, tenantID, slug, resource, ref.App.SchemaName, ref.Entity.Table, ref.App.Blueprint.Revision(),
|
||||
)
|
||||
return ref, nil
|
||||
}
|
||||
|
||||
func (l *CrudLogic) GetBlueprint(slug string) (any, error) {
|
||||
tenantID := authx.TenantID(l.ctx)
|
||||
app, err := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug)
|
||||
|
||||
@@ -230,11 +230,18 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
mergeRes, err = blueprint.MergeInto(bp, incoming)
|
||||
if err != nil {
|
||||
if isNothingNewToPublish(err) {
|
||||
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
|
||||
if needsSchemaIsolation(existing, authx.Role(l.ctx)) {
|
||||
publishMode = "storage_isolated"
|
||||
mergeRes = nil
|
||||
} else {
|
||||
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
publishMode = "pages_added"
|
||||
}
|
||||
publishMode = "pages_added"
|
||||
case mode == "replace":
|
||||
bp = incoming
|
||||
if exists {
|
||||
@@ -253,11 +260,18 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
mergeRes, err = blueprint.MergeInto(bp, incoming)
|
||||
if err != nil {
|
||||
if isNothingNewToPublish(err) {
|
||||
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
|
||||
if needsSchemaIsolation(existing, authx.Role(l.ctx)) {
|
||||
publishMode = "storage_isolated"
|
||||
mergeRes = nil
|
||||
} else {
|
||||
return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
publishMode = "pages_added"
|
||||
}
|
||||
publishMode = "pages_added"
|
||||
} else {
|
||||
bp = incoming
|
||||
publishMode = "created"
|
||||
@@ -278,26 +292,37 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
|
||||
schemaName := bp.AssignSchemaName(tenantID)
|
||||
dbName := bp.AssignDatabaseName(tenantID)
|
||||
// Z8c:智能体若绑定了合法 database_name,新建模块优先落到该库(database_per_app)
|
||||
boundAgentDatabase := false
|
||||
// Z8c/Z39:智能体绑定库可由多个模块共享,因此库内仍须按 tenant+slug 分 Schema,
|
||||
// 不能把所有模块都放进 public 并按同名 entity table 复用。
|
||||
// Z35:中文昵称等非法名不得用于 EnsureDatabase,回落 AssignDatabaseName
|
||||
if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil {
|
||||
if aid := authx.AgentID(l.ctx); aid > 0 {
|
||||
if acc, err := l.svcCtx.Agents.Get(l.ctx, tenantID, aid); err == nil && acc != nil {
|
||||
if dn := strings.TrimSpace(acc.DatabaseName); dn != "" && schema.ValidDBName(dn) {
|
||||
if !exists || existing.DatabaseName == "" {
|
||||
bp.Storage.Mode = "database_per_app"
|
||||
schemaName = bp.AssignSchemaName(tenantID)
|
||||
dbName = dn
|
||||
bp.Storage.Mode = "schema_per_app"
|
||||
schemaName = bp.AssignSchemaName(tenantID)
|
||||
dbName = dn
|
||||
if exists && existing.DatabaseName != "" {
|
||||
dbName = existing.DatabaseName
|
||||
}
|
||||
boundAgentDatabase = true
|
||||
} else if dn != "" && !schema.ValidDBName(dn) {
|
||||
// 历史脏数据(如 DisplayName 当库名):纠正智能体落点库名,避免反复 400
|
||||
safeName := schema.SanitizeDBName(tenantID, slug)
|
||||
_, _ = l.svcCtx.Agents.Update(l.ctx, tenantID, aid, agentstore.UpdateInput{
|
||||
DatabaseName: &safeName,
|
||||
})
|
||||
bp.Storage.Mode = "database_per_app"
|
||||
bp.Storage.Mode = "schema_per_app"
|
||||
schemaName = bp.AssignSchemaName(tenantID)
|
||||
dbName = safeName
|
||||
boundAgentDatabase = true
|
||||
} else if exists && existing.DatabaseName != "" {
|
||||
// 已发布模块仍位于智能体绑定库;重发时一并升级旧 public Schema。
|
||||
bp.Storage.Mode = "schema_per_app"
|
||||
schemaName = bp.AssignSchemaName(tenantID)
|
||||
dbName = existing.DatabaseName
|
||||
boundAgentDatabase = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +339,9 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
appID = existing.AppID
|
||||
now = existing.CreatedAt
|
||||
republish = existing.Status == meta.StatusPublished
|
||||
if existing.SchemaName != "" {
|
||||
// Z39:旧版把智能体同库下的所有模块放入 public,导致同名资源串表。
|
||||
// 重发时切到独立 app_t{tenant}_{slug};数据由既有双向同步重新灌入。
|
||||
if existing.SchemaName != "" && !(boundAgentDatabase && existing.SchemaName == "public") {
|
||||
schemaName = existing.SchemaName
|
||||
bp.Storage.SchemaName = existing.SchemaName
|
||||
}
|
||||
@@ -415,6 +442,7 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
detail := map[string]any{
|
||||
"slug": slug, "schema": schemaName, "database": dbName, "user_id": userID,
|
||||
"republish": republish, "publish_mode": publishMode,
|
||||
"blueprint_revision": bp.Revision(), "resource_tables": resourceTables(bp),
|
||||
}
|
||||
if mergeRes != nil {
|
||||
detail["added_pages"] = mergeRes.AddedPages
|
||||
@@ -465,21 +493,23 @@ func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.Publi
|
||||
}
|
||||
|
||||
resp := &types.PublishResp{
|
||||
AppID: rec.AppID,
|
||||
Slug: slug,
|
||||
SchemaName: schemaName,
|
||||
DatabaseName: dbName,
|
||||
Status: string(meta.StatusPublished),
|
||||
Endpoints: endpoints,
|
||||
DDL: ddl,
|
||||
MemoryMode: l.svcCtx.MemoryMode,
|
||||
PublishMode: publishMode,
|
||||
ModuleName: moduleName,
|
||||
PublishStyle: publishStyle,
|
||||
AccessPath: accessPath,
|
||||
AccessURL: accessURL,
|
||||
PublishedAt: rec.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
OwnerID: ownerID,
|
||||
AppID: rec.AppID,
|
||||
Slug: slug,
|
||||
SchemaName: schemaName,
|
||||
DatabaseName: dbName,
|
||||
Status: string(meta.StatusPublished),
|
||||
Endpoints: endpoints,
|
||||
DDL: ddl,
|
||||
MemoryMode: l.svcCtx.MemoryMode,
|
||||
PublishMode: publishMode,
|
||||
ModuleName: moduleName,
|
||||
PublishStyle: publishStyle,
|
||||
AccessPath: accessPath,
|
||||
AccessURL: accessURL,
|
||||
PublishedAt: rec.UpdatedAt.UTC().Format(time.RFC3339),
|
||||
OwnerID: ownerID,
|
||||
BlueprintRevision: bp.Revision(),
|
||||
ResourceTables: resourceTables(bp),
|
||||
}
|
||||
if mergeRes != nil {
|
||||
resp.AddedPages = mergeRes.AddedPages
|
||||
@@ -500,6 +530,13 @@ func isNothingNewToPublish(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "nothing new to publish")
|
||||
}
|
||||
|
||||
func needsSchemaIsolation(existing *meta.AppRecord, role string) bool {
|
||||
return role == authx.RoleAgent &&
|
||||
existing != nil &&
|
||||
existing.DatabaseName != "" &&
|
||||
existing.SchemaName == "public"
|
||||
}
|
||||
|
||||
// handleNoBlueprintDelta Z36:merge 无增量时,有 module_name 则仅更新 host_meta;否则中文业务错误。
|
||||
func (l *PublishLogic) handleNoBlueprintDelta(
|
||||
existing *meta.AppRecord,
|
||||
@@ -585,20 +622,22 @@ func (l *PublishLogic) publishHostMetaOnly(
|
||||
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,
|
||||
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,
|
||||
BlueprintRevision: bp.Revision(),
|
||||
ResourceTables: resourceTables(bp),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -635,3 +674,18 @@ func buildEndpoints(bp *blueprint.Blueprint) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resourceTables(bp *blueprint.Blueprint) map[string]string {
|
||||
out := make(map[string]string, len(bp.Apis.Resources))
|
||||
entities := make(map[string]string, len(bp.Entities))
|
||||
for _, entity := range bp.Entities {
|
||||
entities[entity.Name] = entity.Table
|
||||
}
|
||||
for _, resource := range bp.Apis.Resources {
|
||||
path := strings.TrimPrefix(resource.Path, "/")
|
||||
if table := entities[resource.Entity]; path != "" && table != "" {
|
||||
out[path] = bp.Storage.SchemaName + "." + table
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -15,31 +15,33 @@ type PublishReq struct {
|
||||
}
|
||||
|
||||
type PublishHostMeta struct {
|
||||
ModuleName string `json:"module_name,omitempty"` // 模块名称(宿主表格「应用/模块名称」)
|
||||
PublishStyle string `json:"publish_style,omitempty"` // 如 immediate → 立即发布上线
|
||||
HostBaseURL string `json:"host_base_url,omitempty"` // 宿主域名,如 https://whm123.yuheng.com
|
||||
ModuleName string `json:"module_name,omitempty"` // 模块名称(宿主表格「应用/模块名称」)
|
||||
PublishStyle string `json:"publish_style,omitempty"` // 如 immediate → 立即发布上线
|
||||
HostBaseURL string `json:"host_base_url,omitempty"` // 宿主域名,如 https://whm123.yuheng.com
|
||||
}
|
||||
|
||||
type PublishResp struct {
|
||||
AppID string `json:"app_id"`
|
||||
Slug string `json:"slug"`
|
||||
SchemaName string `json:"schema_name"`
|
||||
DatabaseName string `json:"database_name,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Endpoints []string `json:"endpoints"`
|
||||
DDL []string `json:"ddl,omitempty"`
|
||||
MemoryMode bool `json:"memory_mode"`
|
||||
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"`
|
||||
AppID string `json:"app_id"`
|
||||
Slug string `json:"slug"`
|
||||
SchemaName string `json:"schema_name"`
|
||||
DatabaseName string `json:"database_name,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Endpoints []string `json:"endpoints"`
|
||||
DDL []string `json:"ddl,omitempty"`
|
||||
MemoryMode bool `json:"memory_mode"`
|
||||
PublishMode string `json:"publish_mode"` // created | pages_added | replaced | host_meta_updated | storage_isolated
|
||||
AddedPages []string `json:"added_pages,omitempty"`
|
||||
AddedEntities []string `json:"added_entities,omitempty"`
|
||||
AddedResources []string `json:"added_resources,omitempty"`
|
||||
BlueprintRevision string `json:"blueprint_revision,omitempty"`
|
||||
ResourceTables map[string]string `json:"resource_tables,omitempty"`
|
||||
// 宿主建站回执(对应「AI 表格数据」)
|
||||
ModuleName string `json:"module_name,omitempty"`
|
||||
PublishStyle string `json:"publish_style,omitempty"`
|
||||
AccessPath string `json:"access_path,omitempty"` // 加密文件路径,如 m/ajzm1_...
|
||||
AccessURL string `json:"access_url,omitempty"` // 可打开的地址(宿主域名或平台公开地址)
|
||||
PublishedAt string `json:"published_at,omitempty"` // RFC3339
|
||||
OwnerID int64 `json:"owner_id,omitempty"` // 用于路径加密的用户/智能体 id
|
||||
ModuleName string `json:"module_name,omitempty"`
|
||||
PublishStyle string `json:"publish_style,omitempty"`
|
||||
AccessPath string `json:"access_path,omitempty"` // 加密文件路径,如 m/ajzm1_...
|
||||
AccessURL string `json:"access_url,omitempty"` // 可打开的地址(宿主域名或平台公开地址)
|
||||
PublishedAt string `json:"published_at,omitempty"` // RFC3339
|
||||
OwnerID int64 `json:"owner_id,omitempty"` // 用于路径加密的用户/智能体 id
|
||||
}
|
||||
|
||||
type AppListItem struct {
|
||||
@@ -85,24 +87,24 @@ type TokenReq struct {
|
||||
}
|
||||
|
||||
type TokenResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
UsernameLoginDisabled bool `json:"username_login_disabled,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
OrgUnitID int64 `json:"org_unit_id,omitempty"`
|
||||
Status string `json:"status,omitempty"` // pending | active
|
||||
AgentKey string `json:"agent_key"`
|
||||
AgentID int64 `json:"agent_id,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
AppSlugs []string `json:"app_slugs,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
TenantName string `json:"tenant_name,omitempty"` // 超管打开某公司管理视图时带回
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
UsernameLoginDisabled bool `json:"username_login_disabled,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
OrgUnitID int64 `json:"org_unit_id,omitempty"`
|
||||
Status string `json:"status,omitempty"` // pending | active
|
||||
AgentKey string `json:"agent_key"`
|
||||
AgentID int64 `json:"agent_id,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
AppSlugs []string `json:"app_slugs,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
TenantName string `json:"tenant_name,omitempty"` // 超管打开某公司管理视图时带回
|
||||
// Z12a:同步落点(智能体/已绑用户)
|
||||
ChannelID string `json:"channel_id,omitempty"`
|
||||
OnlineDBID string `json:"online_db_id,omitempty"`
|
||||
@@ -154,7 +156,6 @@ type RoleUpdateReq struct {
|
||||
Permissions *[]string `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
// AgentSelfRegisterReq 宿主首次连接自注册(公开)。
|
||||
type AgentSelfRegisterReq struct {
|
||||
Name string `json:"name"`
|
||||
@@ -236,11 +237,11 @@ type ImportResp struct {
|
||||
}
|
||||
|
||||
type AggregateResp struct {
|
||||
Total int `json:"total"`
|
||||
GroupBy string `json:"group_by,omitempty"`
|
||||
Buckets []AggregateBucket `json:"buckets,omitempty"`
|
||||
SumField string `json:"sum_field,omitempty"`
|
||||
Sum float64 `json:"sum,omitempty"`
|
||||
Total int `json:"total"`
|
||||
GroupBy string `json:"group_by,omitempty"`
|
||||
Buckets []AggregateBucket `json:"buckets,omitempty"`
|
||||
SumField string `json:"sum_field,omitempty"`
|
||||
Sum float64 `json:"sum,omitempty"`
|
||||
}
|
||||
|
||||
type AggregateBucket struct {
|
||||
@@ -250,10 +251,10 @@ type AggregateBucket struct {
|
||||
}
|
||||
|
||||
type AuditListResp struct {
|
||||
Items []any `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Items []any `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type UploadResp struct {
|
||||
|
||||
Reference in New Issue
Block a user