Files

462 lines
14 KiB
Go
Raw Permalink Normal View History

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
}