WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||||
apperrors "github.com/Tencent/WeKnora/internal/errors"
|
||||
"strings"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// FilterDigitalHumanAgents protects persona-bearing native agents in ordinary
|
||||
// tenant access paths. Explicit organization shares use their verified share path.
|
||||
func (r *customAgentRepository) FilterDigitalHumanAgents(ctx context.Context, agents []*types.CustomAgent) ([]*types.CustomAgent, error) {
|
||||
if len(agents) == 0 {
|
||||
return agents, nil
|
||||
}
|
||||
ids := make([]string, 0, len(agents))
|
||||
for _, agent := range agents {
|
||||
ids = append(ids, agent.ID)
|
||||
}
|
||||
var bindings []types.DigitalHumanBinding
|
||||
if err := r.db.WithContext(ctx).Where("agent_id IN ?", ids).Find(&bindings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byAgent := make(map[string]types.DigitalHumanBinding, len(bindings))
|
||||
humanIDs := make([]string, 0, len(bindings))
|
||||
for _, binding := range bindings {
|
||||
byAgent[binding.AgentID] = binding
|
||||
humanIDs = append(humanIDs, binding.DigitalHumanID)
|
||||
}
|
||||
userID, _ := types.UserIDFromContext(ctx)
|
||||
tenantID, _ := types.TenantIDFromContext(ctx)
|
||||
var humans []types.DigitalHuman
|
||||
if len(humanIDs) > 0 {
|
||||
if err := r.db.WithContext(ctx).Select("id", "name", "active_version").Where("tenant_id = ? AND id IN ?", tenantID, humanIDs).Find(&humans).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
byHuman := make(map[string]types.DigitalHuman, len(humans))
|
||||
for _, human := range humans {
|
||||
byHuman[human.ID] = human
|
||||
}
|
||||
admin := types.IsSystemAdminFromContext(ctx) || types.TenantRoleFromContext(ctx).HasPermission(types.TenantRoleAdmin)
|
||||
result := make([]*types.CustomAgent, 0, len(agents))
|
||||
for _, agent := range agents {
|
||||
binding, bound := byAgent[agent.ID]
|
||||
// Copies preserve the persona snapshot but do not have a live binding.
|
||||
// Keep those snapshots private until the creator removes that persona.
|
||||
ownerID, ownerTenant := binding.OwnerID, binding.TenantID
|
||||
private := bound || strings.Contains(agent.Config.SystemPrompt, "<!-- weknora:digital-human:")
|
||||
if !bound {
|
||||
ownerID, ownerTenant = agent.CreatedBy, agent.TenantID
|
||||
}
|
||||
shared := accesspolicy.Readable(ctx, "agents", agent.ID, false, false) || accesspolicy.CanExecuteDependency(ctx, agent.TenantID, "agents", agent.ID, agent.CreatedBy) || (bound && accesspolicy.Readable(ctx, "digital-humans", binding.DigitalHumanID, false, false))
|
||||
if !private || (tenantID == ownerTenant && (admin || shared || (userID != "" && !types.IsSyntheticUserID(userID) && userID == ownerID))) {
|
||||
agent.DigitalHuman = nil
|
||||
if human, ok := byHuman[binding.DigitalHumanID]; bound && ok {
|
||||
agent.DigitalHuman = &types.AgentDigitalHumanSummary{ID: human.ID, Name: human.Name, AppliedVersion: binding.AppliedVersion, ActiveVersion: human.ActiveVersion}
|
||||
}
|
||||
result = append(result, agent)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Re-read the binding and persona before each execution. A user's installed
|
||||
// snapshot does not turn a revoked shared persona into their own resource.
|
||||
func (r *customAgentRepository) CheckDigitalHumanAgentExecution(ctx context.Context, agent *types.CustomAgent) error {
|
||||
if !strings.Contains(agent.Config.SystemPrompt, "<!-- weknora:digital-human:") {
|
||||
return nil
|
||||
}
|
||||
denied := apperrors.NewForbiddenError("此 Agent 的数字人已删除、解绑或授权已撤销,请重新选择可用数字人")
|
||||
if tenant, ok := types.TenantIDFromContext(ctx); !ok || tenant != agent.TenantID || tenant == 0 {
|
||||
return denied
|
||||
}
|
||||
var binding types.DigitalHumanBinding
|
||||
if err := r.db.WithContext(ctx).Where("tenant_id = ? AND agent_id = ? AND owner_id = ?", agent.TenantID, agent.ID, agent.CreatedBy).First(&binding).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return denied
|
||||
}
|
||||
return err
|
||||
}
|
||||
var human types.DigitalHuman
|
||||
if err := r.db.WithContext(ctx).Where("tenant_id = ? AND id = ?", agent.TenantID, binding.DigitalHumanID).First(&human).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return denied
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(agent.Config.SystemPrompt, fmt.Sprintf("<!-- weknora:digital-human:%s:start -->", human.ID)) || strings.Count(agent.Config.SystemPrompt, "<!-- weknora:digital-human:") != 2 {
|
||||
return denied
|
||||
}
|
||||
if binding.AppliedVersion <= 0 || binding.AppliedVersion != human.ActiveVersion || !accesspolicy.Allowed(ctx, "digital-humans.read") || accesspolicy.Hidden(ctx, "digital-humans", human.ID) {
|
||||
return denied
|
||||
}
|
||||
userID, _ := types.UserIDFromContext(ctx)
|
||||
admin := types.IsSystemAdminFromContext(ctx) || types.TenantRoleFromContext(ctx).HasPermission(types.TenantRoleAdmin)
|
||||
ownedDependency := human.OwnerID != "" && human.OwnerID == agent.CreatedBy
|
||||
if !admin && !ownedDependency && !accesspolicy.Readable(ctx, "digital-humans", human.ID, human.OwnerID == userID, false) {
|
||||
return denied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *tenantSkillRepository) CheckDigitalHumanAgentExecution(ctx context.Context, agent *types.CustomAgent) error {
|
||||
return (&customAgentRepository{db: r.db}).CheckDigitalHumanAgentExecution(ctx, agent)
|
||||
}
|
||||
|
||||
// An allowed copy must keep its live persona binding so later review,
|
||||
// revocation and deletion still apply to the installed profile.
|
||||
func (r *customAgentRepository) CreateAgentCopy(ctx context.Context, sourceID string, target *types.CustomAgent) error {
|
||||
if !strings.Contains(target.Config.SystemPrompt, "<!-- weknora:digital-human:") {
|
||||
return r.CreateAgent(ctx, target)
|
||||
}
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var original types.DigitalHumanBinding
|
||||
if err := tx.Where("tenant_id = ? AND agent_id = ?", target.TenantID, sourceID).First(&original).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var human types.DigitalHuman
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("tenant_id = ? AND id = ?", target.TenantID, original.DigitalHumanID).First(&human).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var source types.CustomAgent
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("tenant_id = ? AND id = ?", target.TenantID, sourceID).First(&source).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var current types.DigitalHumanBinding
|
||||
if err := tx.Where("tenant_id = ? AND agent_id = ?", target.TenantID, sourceID).First(¤t).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if current.DigitalHumanID != human.ID || current.AppliedVersion != human.ActiveVersion {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
repo := &customAgentRepository{db: tx}
|
||||
if err := repo.CheckDigitalHumanAgentCopy(ctx, sourceID, target.TenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repo.CheckDigitalHumanAgentExecution(ctx, &source); err != nil {
|
||||
return err
|
||||
}
|
||||
if source.Config.SystemPrompt != target.Config.SystemPrompt {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
if err := tx.Create(target).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
binding := types.DigitalHumanBinding{ID: uuid.NewString(), TenantID: target.TenantID, OwnerID: target.CreatedBy, DigitalHumanID: human.ID, AgentID: target.ID, BasePrompt: removeDigitalHumanPrompt(target.Config.SystemPrompt, human.ID), AppliedVersion: human.ActiveVersion}
|
||||
return tx.Create(&binding).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *customAgentRepository) CheckDigitalHumanAgentCopy(ctx context.Context, id string, tenantID uint64) error {
|
||||
if types.IsSystemAdminFromContext(ctx) || types.TenantRoleFromContext(ctx).HasPermission(types.TenantRoleAdmin) {
|
||||
return nil
|
||||
}
|
||||
userID, _ := types.UserIDFromContext(ctx)
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Table("digital_human_bindings AS binding").
|
||||
Joins("JOIN digital_humans AS human ON human.id = binding.digital_human_id AND human.tenant_id = binding.tenant_id").
|
||||
Where("binding.agent_id = ? AND binding.tenant_id = ? AND human.owner_id <> ?", id, tenantID, userID).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return apperrors.NewForbiddenError("此 Agent 挂载了共享数字人,只能使用,不能复制其档案")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *customAgentRepository) CheckDigitalHumanAgentDeletion(ctx context.Context, id string, tenantID uint64) error {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(&types.DigitalHuman{}).Where("primary_agent_id = ? AND tenant_id = ?", id, tenantID).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return types.ErrDigitalHumanPrimaryAgent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *customAgentRepository) DeleteAgentWithDigitalHumanBindings(ctx context.Context, id string, tenantID uint64) ([]string, error) {
|
||||
var channelIDs []string
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
repo := &customAgentRepository{db: tx}
|
||||
if err := repo.CheckDigitalHumanAgentDeletion(ctx, id, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repo.DeleteAgent(ctx, id, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := detachFusionAgent(tx, id, tenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
channelIDs, err = softDeleteAgentIMChannels(tx, id, tenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("agent_id = ? AND tenant_id = ?", id, tenantID).Delete(&types.DigitalHumanBinding{}).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return channelIDs, nil
|
||||
}
|
||||
|
||||
func detachFusionAgent(tx *gorm.DB, id string, tenantID uint64) error {
|
||||
if tx.Dialector.Name() == "postgres" {
|
||||
return tx.Exec(`UPDATE custom_agents SET config = jsonb_set(config::jsonb, '{fusion_agent_ids}', (config::jsonb->'fusion_agent_ids') - ?), updated_at = CURRENT_TIMESTAMP WHERE tenant_id = ? AND deleted_at IS NULL AND jsonb_typeof(config::jsonb->'fusion_agent_ids') = 'array' AND jsonb_exists(config::jsonb->'fusion_agent_ids', ?)`, id, tenantID, id).Error
|
||||
}
|
||||
var agents []types.CustomAgent
|
||||
if err := tx.Where("tenant_id = ?", tenantID).Find(&agents).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, agent := range agents {
|
||||
ids := make([]string, 0, len(agent.Config.FusionAgentIDs))
|
||||
for _, fusionID := range agent.Config.FusionAgentIDs {
|
||||
if fusionID != id {
|
||||
ids = append(ids, fusionID)
|
||||
}
|
||||
}
|
||||
if len(ids) != len(agent.Config.FusionAgentIDs) {
|
||||
agent.Config.FusionAgentIDs = ids
|
||||
if err := tx.Model(&agent).Update("config", agent.Config).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user