462 lines
14 KiB
Go
462 lines
14 KiB
Go
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
|
||
|
|
}
|