package accesscontrol import ( "context" "encoding/json" "fmt" "strings" "time" "unicode/utf8" "github.com/Tencent/WeKnora/internal/accesspolicy" "github.com/Tencent/WeKnora/internal/application/repository" "github.com/Tencent/WeKnora/internal/types" "github.com/Tencent/WeKnora/internal/workbench" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" ) // KnowledgeVersionAdapter performs external work only in the durable worker. // HTTP transactions reserve jobs; they never pretend file/index copies roll back. type KnowledgeVersionAdapter interface { CloneKnowledgeVersion(context.Context, string, string, func(int, int) error) error } func (s *Service) SetKnowledgeVersionAdapter(adapter KnowledgeVersionAdapter) { s.versionKnowledge = adapter } type agentVersion struct { Name string `json:"name"` Description string `json:"description"` Avatar string `json:"avatar"` Config types.CustomAgentConfig `json:"config"` UpdatedAt time.Time `json:"updated_at"` } type skillVersion struct { Name string `json:"name"` Description string `json:"description"` Version string `json:"version"` Instructions string `json:"instructions"` UpdatedAt time.Time `json:"updated_at"` } type humanVersion struct { Name string `json:"name"` Description string `json:"description"` Avatar string `json:"avatar"` ModelID string `json:"model_id"` Persona map[string]string `json:"persona"` Revision int64 `json:"revision"` } type flowVersion struct { Name string `json:"name"` Data workbench.FlowData `json:"data"` Version int `json:"version"` } func (s *Service) resourceSnapshot(tx *gorm.DB, ctx context.Context, kind, id string) (types.JSON, string, error) { t, _ := identity(ctx) switch kind { case "agents": var v types.CustomAgent if err := tx.Where("tenant_id=? AND id=?", t, id).First(&v).Error; err != nil { return nil, "", err } return fingerprint(agentVersion{v.Name, v.Description, v.Avatar, v.Config, v.UpdatedAt}) case "skills": var v types.TenantSkillCatalogEntity if err := tx.Where("tenant_id=? AND id=? AND instruction_only=true", t, id).First(&v).Error; err != nil { return nil, "", err } return fingerprint(skillVersion{v.Name, v.Description, v.Version, v.Instructions, v.UpdatedAt}) case "digital-humans": var v types.DigitalHuman if err := tx.Where("tenant_id=? AND id=?", t, id).First(&v).Error; err != nil { return nil, "", err } if v.ProcessingJobID != "" { return nil, "", fmt.Errorf("%w: 数字人正在蒸馏", errConflict) } var version types.DigitalHumanVersion if err := tx.Where("tenant_id=? AND digital_human_id=? AND number=?", t, id, v.LatestVersion).First(&version).Error; err != nil { return nil, "", err } return fingerprint(humanVersion{v.Name, v.Description, v.Avatar, v.ModelID, version.Persona, v.Revision}) case "flows": var v workbench.Resource if err := tx.Where("tenant_id=? AND id=? AND kind='flows'", t, id).First(&v).Error; err != nil { return nil, "", err } var d workbench.FlowData b, err := json.Marshal(v.Data) if err != nil { return nil, "", err } if err = json.Unmarshal(b, &d); err != nil { return nil, "", err } return fingerprint(flowVersion{v.Name, d, v.Version}) case "knowledge": if s.versionKnowledge == nil { return nil, "", fmt.Errorf("%w: 知识库个人版本复制服务尚未配置", errInvalid) } v, err := knowledgeVersionSnapshot(ctx, tx, id) if err != nil { return nil, "", err } return fingerprint(v) } return nil, "", errInvalid } func (s *Service) cloneVersionResource(tx *gorm.DB, ctx context.Context, kind, source, target string) error { t, u := identity(ctx) switch kind { case "agents": var v types.CustomAgent if err := tx.Where("tenant_id=? AND id=?", t, source).First(&v).Error; err != nil { return err } var bound int64 if err := tx.Model(&types.DigitalHumanBinding{}).Where("tenant_id=? AND agent_id=?", t, source).Count(&bound).Error; err != nil { return err } if bound > 0 { return fmt.Errorf("%w: 这个 Agent 已绑定数字人,请创建该数字人的个人版本", errInvalid) } v.ID = target v.CreatedBy = u v.Visibility = "private" v.IsBuiltin = false v.CreatedAt = time.Time{} v.UpdatedAt = time.Time{} v.DeletedAt = gorm.DeletedAt{} return tx.Create(&v).Error case "skills": var v types.TenantSkillCatalogEntity if err := tx.Where("tenant_id=? AND id=? AND instruction_only=true", t, source).First(&v).Error; err != nil { return err } v.ID = target v.CreatedBy = u v.Visibility = "private" v.BundleRef = "" v.BundleSHA256 = "" v.VersionSourceID = source v.CreatedAt = time.Time{} v.UpdatedAt = time.Time{} v.DeletedAt = gorm.DeletedAt{} return tx.Create(&v).Error case "digital-humans": body, _, err := s.resourceSnapshot(tx, ctx, kind, source) if err != nil { return err } var v humanVersion if err = json.Unmarshal(body, &v); err != nil { return err } h := types.DigitalHuman{ID: target, TenantID: t, OwnerID: u, Name: v.Name, Description: v.Description, Avatar: v.Avatar, ModelID: v.ModelID, Status: "draft", LatestVersion: 1, PrimaryAgentID: uuid.NewString(), Revision: 1} return repository.NewDigitalHumanRepository(tx).Create(ctx, &h, v.Persona) case "flows": var v workbench.Resource if err := tx.Where("tenant_id=? AND id=? AND kind='flows'", t, source).First(&v).Error; err != nil { return err } v.ID = target v.OwnerID = u v.OwnerName = "" v.Version = 1 v.CreatedAt = time.Time{} v.UpdatedAt = time.Time{} return tx.Create(&v).Error case "knowledge": if s.versionKnowledge == nil { return errInvalid } return fmt.Errorf("%w: 知识库复制必须使用后台任务", errInvalid) } return errInvalid } func (s *Service) applyVersionResource(tx *gorm.DB, ctx context.Context, kind, id string, body types.JSON) error { t, _ := identity(ctx) switch kind { case "agents": var p agentVersion if err := json.Unmarshal(body, &p); err != nil { return err } var v types.CustomAgent if err := tx.Where("tenant_id=? AND id=?", t, id).First(&v).Error; err != nil { return err } var bindings int64 if err := tx.Model(&types.DigitalHumanBinding{}).Where("tenant_id=? AND agent_id=?", t, id).Count(&bindings).Error; err != nil { return err } if bindings > 0 { return fmt.Errorf("%w: 原 Agent 已绑定数字人,请审核数字人版本", errConflict) } v.Name = p.Name v.Description = p.Description v.Avatar = p.Avatar v.Config = p.Config return tx.Save(&v).Error case "skills": var p skillVersion if err := json.Unmarshal(body, &p); err != nil { return err } return tx.Model(&types.TenantSkillCatalogEntity{}).Where("tenant_id=? AND id=? AND instruction_only=true", t, id).Updates(map[string]any{"name": p.Name, "description": p.Description, "version": p.Version, "instructions": p.Instructions}).Error case "digital-humans": var p humanVersion if err := json.Unmarshal(body, &p); err != nil { return err } var h types.DigitalHuman if err := tx.Where("tenant_id=? AND id=?", t, id).First(&h).Error; err != nil { return err } r := repository.NewDigitalHumanRepository(tx) scope := types.DigitalHumanScope{TenantID: t, OwnerID: h.OwnerID} if err := r.Update(ctx, scope, id, h.Revision, p.Name, p.Description, p.Avatar, p.ModelID, p.Persona); err != nil { return err } return r.Review(ctx, scope, id, h.LatestVersion+1, true, "全局个人版本审核通过") case "flows": var p flowVersion if err := json.Unmarshal(body, &p); err != nil { return err } var v workbench.Resource if err := tx.Where("tenant_id=? AND id=? AND kind='flows'", t, id).First(&v).Error; err != nil { return err } var jobs int64 if err := tx.Model(&workbench.Job{}).Where("tenant_id=? AND resource_id=? AND status IN ?", t, id, []string{"queued", "running"}).Count(&jobs).Error; err != nil { return err } if jobs > 0 { return errConflict } if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&workbench.Revision{ID: uuid.NewString(), TenantID: t, ResourceID: id, Version: v.Version, ActorID: v.OwnerID, Data: v.Data}).Error; err != nil { return err } b, err := json.Marshal(p.Data) if err != nil { return err } if err = json.Unmarshal(b, &v.Data); err != nil { return err } v.Name = p.Name v.Version++ return tx.Save(&v).Error case "knowledge": if s.versionKnowledge == nil { return errInvalid } return fmt.Errorf("%w: 请在知识库页面编辑内容,再提交不可变快照审核", errInvalid) } return errInvalid } func (s *Service) validateVersionReferences(tx *gorm.DB, ctx context.Context, kind string, body types.JSON, publishing bool) error { t, u := identity(ctx) check := func(kind, id string) error { if id == "" { return nil } if !publishing { if err := versionPermission(ctx, kind, false); err != nil { return err } } v, err := sourceView(tx, t, kind, id) if err != nil { return err } if publishing && !v.Global { return fmt.Errorf("%w: 全局版本不能引用未发布的 %s 资源", errInvalid, kind) } if !publishing && !administrator(ctx) && !accesspolicy.Readable(ctx, kind, id, v.OwnerID == u, v.Global) { return errVersionDenied } return nil } model := func(id string) error { if id == "" { return nil } var count int64 if err := tx.Table("models").Where("tenant_id=? AND id=? AND deleted_at IS NULL", t, id).Count(&count).Error; err != nil { return err } if count != 1 { return fmt.Errorf("%w: 模型不存在或不属于当前空间", errInvalid) } return nil } switch kind { case "agents": var v agentVersion if err := json.Unmarshal(body, &v); err != nil { return err } for _, id := range v.Config.KnowledgeBases { if err := check("knowledge", id); err != nil { return err } } for _, id := range v.Config.InstructionSkillIDs { if err := check("skills", id); err != nil { return err } } for _, id := range []string{v.Config.ModelID, v.Config.RerankModelID, v.Config.VLMModelID, v.Config.ASRModelID, v.Config.QueryUnderstandModelID} { if err := model(id); err != nil { return err } } for table, id := range map[string]string{"tenant_sandbox_configs": v.Config.SandboxConfigID, "web_search_providers": v.Config.WebSearchProviderID} { if id == "" { continue } var n int64 if err := tx.Table(table).Where("tenant_id=? AND id=? AND deleted_at IS NULL", t, id).Count(&n).Error; err != nil { return err } if n != 1 { return errInvalid } } for _, id := range v.Config.MCPServices { var n int64 if err := tx.Table("mcp_services").Where("tenant_id=? AND id=? AND deleted_at IS NULL", t, id).Count(&n).Error; err != nil { return err } if n != 1 { return errInvalid } } case "digital-humans": var v humanVersion if err := json.Unmarshal(body, &v); err != nil { return err } return model(v.ModelID) case "flows": var v flowVersion if err := json.Unmarshal(body, &v); err != nil { return err } for _, n := range v.Data.Nodes { if err := check("agents", n.AgentID); err != nil { return err } } for _, id := range v.Data.KBIDs { if err := check("knowledge", id); err != nil { return err } } for _, id := range v.Data.KnowledgeIDs { var k types.Knowledge if err := tx.Where("tenant_id=? AND id=?", t, id).First(&k).Error; err != nil { return err } if err := check("knowledge", k.KnowledgeBaseID); err != nil { return err } } for kb := range v.Data.KnowledgeFolders { if err := check("knowledge", kb); err != nil { return err } } for kb := range v.Data.KnowledgeTags { if err := check("knowledge", kb); err != nil { return err } } } return nil } func (s *Service) validateVersionBody(kind string, body types.JSON) error { var base struct { Name string `json:"name"` Description string `json:"description"` } if json.Unmarshal(body, &base) != nil || strings.TrimSpace(base.Name) == "" || utf8.RuneCountInString(base.Name) > 100 || utf8.RuneCountInString(base.Description) > 3000 { return errInvalid } switch kind { case "agents": var v agentVersion if err := json.Unmarshal(body, &v); err != nil { return errInvalid } if len(v.Config.SystemPrompt) > 64000 || len(v.Config.InstructionSkillIDs) > 32 || v.Config.Temperature < 0 || v.Config.Temperature > 2 { return errInvalid } if v.Config.AgentMode != "" && v.Config.AgentMode != types.AgentModeSmartReasoning && v.Config.AgentMode != types.AgentModeQuickAnswer { return errInvalid } if err := v.Config.QuestionSuggestions.Validate(); err != nil { return fmt.Errorf("%w: %s", errInvalid, err) } case "skills": var v skillVersion if err := json.Unmarshal(body, &v); err != nil { return errInvalid } if strings.TrimSpace(v.Instructions) == "" || utf8.RuneCountInString(v.Instructions) > 16000 || len(v.Version) > 64 { return errInvalid } case "digital-humans": var v humanVersion if err := json.Unmarshal(body, &v); err != nil { return errInvalid } keys := map[string]bool{} for _, key := range strings.Fields("persona_name identity tone pace vocab taboo opening_habit emotion_style interaction expertise_border sample_excerpt writing_style narrative_style thinking_style audience_profile hook_patterns pain_point_hooks itch_point_hooks sales_logic cta_transition expression_prompt expression_rules") { keys[key] = true } total := 0 for key, value := range v.Persona { n := utf8.RuneCountInString(value) total += n if !keys[key] || n > 6000 { return errInvalid } } if total == 0 || total > 30000 { return errInvalid } case "flows": var v flowVersion if err := json.Unmarshal(body, &v); err != nil { return errInvalid } if len(v.Data.Nodes) > 1000 || len(v.Data.Edges) > 3000 { return errInvalid } ids := map[string]bool{} for _, n := range v.Data.Nodes { if n.ID == "" || n.AgentID == "" || ids[n.ID] { return errInvalid } ids[n.ID] = true } for _, e := range v.Data.Edges { if !ids[e.Source] || !ids[e.Target] || e.Source == e.Target { return errInvalid } } } return nil }