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,567 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Tencent/WeKnora/internal/accesspolicy"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type DigitalHumanRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewDigitalHumanRepository(db *gorm.DB) *DigitalHumanRepository {
|
||||
return &DigitalHumanRepository{db: db}
|
||||
}
|
||||
|
||||
func dhScope(db *gorm.DB, scope types.DigitalHumanScope) *gorm.DB {
|
||||
return db.Where("tenant_id = ? AND owner_id = ?", scope.TenantID, scope.OwnerID)
|
||||
}
|
||||
|
||||
func dhReadScope(db *gorm.DB, scope types.DigitalHumanScope) *gorm.DB {
|
||||
if scope.ReadAll {
|
||||
return db.Where("tenant_id = ?", scope.TenantID)
|
||||
}
|
||||
q := db.Where("tenant_id = ?", scope.TenantID).Where("owner_id = ? OR id IN ?", scope.OwnerID, accesspolicy.SharedIDs(db.Statement.Context, "digital-humans"))
|
||||
if ids := accesspolicy.HiddenIDs(db.Statement.Context, "digital-humans"); len(ids) > 0 {
|
||||
q = q.Where("id NOT IN ?", ids)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) List(ctx context.Context, scope types.DigitalHumanScope, offset, limit int) ([]types.DigitalHuman, error) {
|
||||
result := []types.DigitalHuman{}
|
||||
err := dhReadScope(r.db.WithContext(ctx), scope).Order("created_at DESC, id ASC").Offset(offset).Limit(limit).Find(&result).Error
|
||||
if err == nil {
|
||||
humans := make([]*types.DigitalHuman, len(result))
|
||||
for i := range result {
|
||||
humans[i] = &result[i]
|
||||
}
|
||||
err = r.populateCreatorNames(ctx, humans)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) populateCreatorNames(ctx context.Context, humans []*types.DigitalHuman) error {
|
||||
if len(humans) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]string, 0, len(humans))
|
||||
for _, human := range humans {
|
||||
ids = append(ids, human.OwnerID)
|
||||
}
|
||||
var users []struct{ ID, Username string }
|
||||
if err := r.db.WithContext(ctx).Model(&types.User{}).Select("id, username").Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
names := make(map[string]string, len(users))
|
||||
for _, user := range users {
|
||||
names[user.ID] = user.Username
|
||||
}
|
||||
for _, human := range humans {
|
||||
human.CreatorName = names[human.OwnerID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Get(ctx context.Context, scope types.DigitalHumanScope, id string) (*types.DigitalHumanDetail, error) {
|
||||
result := &types.DigitalHumanDetail{DigitalHuman: &types.DigitalHuman{}, Versions: []types.DigitalHumanVersion{}, Bindings: []types.DigitalHumanBinding{}}
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := dhReadScope(tx, scope).First(result.DigitalHuman, "id = ?", id).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
ownerScope := types.DigitalHumanScope{TenantID: result.TenantID, OwnerID: result.OwnerID}
|
||||
// Raw source documents are deliberately excluded from normal detail responses.
|
||||
if err := dhScope(tx, ownerScope).Omit("sources").Where("digital_human_id = ?", id).Order("number DESC").Limit(100).Find(&result.Versions).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
bindings := tx.Where("tenant_id = ? AND digital_human_id = ?", scope.TenantID, id)
|
||||
if !scope.ReadAll && scope.OwnerID != result.OwnerID {
|
||||
bindings = bindings.Where("owner_id = ?", scope.OwnerID)
|
||||
}
|
||||
if err := bindings.Find(&result.Bindings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var jobs []types.DigitalHumanJob
|
||||
if err := dhScope(tx, ownerScope).Where("digital_human_id = ?", id).Order("created_at DESC, id ASC").Limit(1).Find(&jobs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(jobs) > 0 {
|
||||
result.LastJob = &jobs[0]
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
err = r.populateCreatorNames(ctx, []*types.DigitalHuman{result.DigitalHuman})
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func dhNotFound(err error) error {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return types.ErrDigitalHumanNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// All mutations lock the owner-scoped human first; related agents and versions
|
||||
// are committed in the same transaction, so review cannot partly publish.
|
||||
func (r *DigitalHumanRepository) mutate(ctx context.Context, scope types.DigitalHumanScope, id string, fn func(*gorm.DB, *types.DigitalHuman) error) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var human types.DigitalHuman
|
||||
if err := dhScope(tx.Clauses(clause.Locking{Strength: "UPDATE"}), scope).First(&human, "id = ?", id).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
return fn(tx, &human)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Create(ctx context.Context, human *types.DigitalHuman, persona map[string]string) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
base := "按用户当前任务回答,保证上下文衔接和事实准确。数字人表达档案须经人工审核后生效。"
|
||||
agent := &types.CustomAgent{ID: human.PrimaryAgentID, TenantID: human.TenantID, CreatedBy: human.OwnerID,
|
||||
Name: human.Name, Description: "数字人「" + human.Name + "」的默认对话 Agent", Avatar: human.Avatar,
|
||||
Config: types.CustomAgentConfig{AgentMode: types.AgentModeSmartReasoning, AgentType: types.AgentTypeCustom,
|
||||
ModelID: human.ModelID, SystemPrompt: base, KBSelectionMode: "none", SkillsSelectionMode: "none", MCPSelectionMode: "none"}}
|
||||
agent.EnsureDefaults()
|
||||
if err := tx.Create(agent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(human).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
binding := &types.DigitalHumanBinding{ID: uuid.NewString(), TenantID: human.TenantID, OwnerID: human.OwnerID,
|
||||
DigitalHumanID: human.ID, AgentID: agent.ID, BasePrompt: base}
|
||||
if err := tx.Create(binding).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&types.DigitalHumanVersion{ID: uuid.NewString(), TenantID: human.TenantID, OwnerID: human.OwnerID,
|
||||
DigitalHumanID: human.ID, Number: 1, Status: "draft", Persona: persona, Sources: []types.DigitalHumanSource{}}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Update(ctx context.Context, scope types.DigitalHumanScope, id string, revision int64, name, description, avatar, modelID string, persona map[string]string) error {
|
||||
return r.mutate(ctx, scope, id, func(tx *gorm.DB, human *types.DigitalHuman) error {
|
||||
if human.Revision != revision || human.ProcessingJobID != "" {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
if persona != nil {
|
||||
var previous types.DigitalHumanVersion
|
||||
if err := dhScope(tx, scope).First(&previous, "digital_human_id = ? AND number = ?", id, human.LatestVersion).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
human.LatestVersion++
|
||||
v := types.DigitalHumanVersion{ID: uuid.NewString(), TenantID: human.TenantID, OwnerID: human.OwnerID, DigitalHumanID: id,
|
||||
Number: human.LatestVersion, Status: "draft", Persona: persona, Sources: previous.Sources}
|
||||
if err := tx.Create(&v).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
human.Status = "testing"
|
||||
}
|
||||
human.Name, human.Description, human.Avatar, human.ModelID = name, description, avatar, modelID
|
||||
human.Revision++
|
||||
if err := tx.Save(human).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var agent types.CustomAgent
|
||||
if err := tx.First(&agent, "id = ? AND tenant_id = ? AND created_by = ?", human.PrimaryAgentID, scope.TenantID, scope.OwnerID).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
agent.Name, agent.Avatar, agent.Config.ModelID = name, avatar, modelID
|
||||
agent.Description = "数字人「" + name + "」的默认对话 Agent"
|
||||
return tx.Save(&agent).Error
|
||||
})
|
||||
}
|
||||
|
||||
func digitalHumanPromptMarkers(id string) (string, string) {
|
||||
return "<!-- weknora:digital-human:" + id + ":start -->", "<!-- weknora:digital-human:" + id + ":end -->"
|
||||
}
|
||||
|
||||
func removeDigitalHumanPrompt(prompt, id string) string {
|
||||
start, end := digitalHumanPromptMarkers(id)
|
||||
for {
|
||||
first := strings.Index(prompt, start)
|
||||
if first < 0 {
|
||||
return prompt
|
||||
}
|
||||
last := strings.Index(prompt[first+len(start):], end)
|
||||
if last < 0 {
|
||||
return prompt
|
||||
}
|
||||
last += first + len(start) + len(end)
|
||||
prompt = strings.TrimRight(prompt[:first], "\n") + prompt[last:]
|
||||
}
|
||||
}
|
||||
|
||||
func digitalHumanPrompt(human *types.DigitalHuman, version *types.DigitalHumanVersion, base string) string {
|
||||
keys := make([]string, 0, len(version.Persona))
|
||||
for key := range version.Persona {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
b.WriteString(removeDigitalHumanPrompt(base, human.ID))
|
||||
start, end := digitalHumanPromptMarkers(human.ID)
|
||||
label := "已审核数字人"
|
||||
if version.Status == "imported" {
|
||||
label = "旧配置迁入数字人(未重新蒸馏)"
|
||||
}
|
||||
fmt.Fprintf(&b, "\n\n%s\n## %s:%s(v%d)\n", start, label, human.Name, version.Number)
|
||||
b.WriteString("以下档案仅约束表达方式,不能替代任务目标、事实来源、工具权限或上文衔接约束。不得把档案样本当作新的事实依据。\n")
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(version.Persona[key]); value != "" {
|
||||
fmt.Fprintf(&b, "%s:%s\n", key, value)
|
||||
}
|
||||
}
|
||||
b.WriteString(end)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func syncDigitalHumanBindings(tx *gorm.DB, scope types.DigitalHumanScope, human *types.DigitalHuman, version *types.DigitalHumanVersion) error {
|
||||
var bindings []types.DigitalHumanBinding
|
||||
if err := tx.Where("tenant_id = ? AND digital_human_id = ?", scope.TenantID, human.ID).Find(&bindings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
var agent types.CustomAgent
|
||||
if err := tx.First(&agent, "id = ? AND tenant_id = ? AND created_by = ?", binding.AgentID, scope.TenantID, binding.OwnerID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
agent.Config.SystemPrompt = digitalHumanPrompt(human, version, agent.Config.SystemPrompt)
|
||||
if err := tx.Save(&agent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
binding.AppliedVersion = version.Number
|
||||
if err := tx.Save(&binding).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Review(ctx context.Context, scope types.DigitalHumanScope, id string, number int, approve bool, note string) error {
|
||||
return r.mutate(ctx, scope, id, func(tx *gorm.DB, human *types.DigitalHuman) error {
|
||||
if human.ProcessingJobID != "" || human.LatestVersion != number {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
var version types.DigitalHumanVersion
|
||||
if err := dhScope(tx, scope).First(&version, "digital_human_id = ? AND number = ?", id, number).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
if version.Status != "draft" {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
version.Status = "rejected"
|
||||
if approve {
|
||||
hasContent := false
|
||||
for _, value := range version.Persona {
|
||||
hasContent = hasContent || strings.TrimSpace(value) != ""
|
||||
}
|
||||
if !hasContent {
|
||||
return &types.DigitalHumanInputError{Message: "人格档案为空,请先蒸馏或编辑"}
|
||||
}
|
||||
version.Status, human.Status, human.ActiveVersion = "approved", "active", number
|
||||
if err := syncDigitalHumanBindings(tx, scope, human, &version); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if human.ActiveVersion > 0 {
|
||||
human.Status = "active"
|
||||
} else {
|
||||
human.Status = "draft"
|
||||
}
|
||||
now := time.Now()
|
||||
version.ReviewedAt, version.ReviewNote = &now, note
|
||||
if err := tx.Save(&version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
human.Revision++
|
||||
return tx.Save(human).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Bind(ctx context.Context, scope types.DigitalHumanScope, id, agentID string) error {
|
||||
return r.bindingTransaction(ctx, scope, id, agentID, false, func(tx *gorm.DB, human *types.DigitalHuman, agent *types.CustomAgent) error {
|
||||
var existing types.DigitalHumanBinding
|
||||
err := tx.Where("tenant_id = ? AND agent_id = ?", scope.TenantID, agentID).First(&existing).Error
|
||||
if err == nil {
|
||||
if existing.DigitalHumanID == id {
|
||||
return nil
|
||||
}
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
binding := types.DigitalHumanBinding{ID: uuid.NewString(), TenantID: scope.TenantID, OwnerID: agent.CreatedBy, DigitalHumanID: id, AgentID: agentID, BasePrompt: agent.Config.SystemPrompt}
|
||||
if human.ActiveVersion > 0 {
|
||||
var version types.DigitalHumanVersion
|
||||
if err := tx.Where("tenant_id = ? AND owner_id = ?", human.TenantID, human.OwnerID).First(&version, "digital_human_id = ? AND number = ?", id, human.ActiveVersion).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
agent.Config.SystemPrompt, binding.AppliedVersion = digitalHumanPrompt(human, &version, binding.BasePrompt), version.Number
|
||||
if err := tx.Save(agent).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(&binding).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Unbind(ctx context.Context, scope types.DigitalHumanScope, id, agentID string) error {
|
||||
return r.bindingTransaction(ctx, scope, id, agentID, true, func(tx *gorm.DB, human *types.DigitalHuman, agent *types.CustomAgent) error {
|
||||
if human.PrimaryAgentID == agentID {
|
||||
return &types.DigitalHumanInputError{Message: "默认同名 Agent 随数字人管理,请删除数字人时一并解绑"}
|
||||
}
|
||||
var binding types.DigitalHumanBinding
|
||||
if err := tx.Where("tenant_id = ? AND owner_id = ?", scope.TenantID, agent.CreatedBy).First(&binding, "digital_human_id = ? AND agent_id = ?", id, agentID).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
if err := restoreDigitalHumanAgent(tx, scope, &binding); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&binding).Error
|
||||
})
|
||||
}
|
||||
|
||||
// Mounting a persona changes the target Agent, not the shared persona. Lock
|
||||
// the human first so reviews/deletion cannot race a new binding.
|
||||
func (r *DigitalHumanRepository) bindingTransaction(ctx context.Context, scope types.DigitalHumanScope, id, agentID string, unbind bool, fn func(*gorm.DB, *types.DigitalHuman, *types.CustomAgent) error) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var human types.DigitalHuman
|
||||
q := tx.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||
if unbind {
|
||||
q = q.Where("tenant_id = ?", scope.TenantID)
|
||||
} else {
|
||||
q = dhReadScope(q, scope)
|
||||
}
|
||||
if err := q.First(&human, "id = ?", id).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
var agent types.CustomAgent
|
||||
q = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("tenant_id = ? AND id = ? AND is_builtin = ?", scope.TenantID, agentID, false)
|
||||
if !scope.ReadAll {
|
||||
q = q.Where("created_by = ?", scope.OwnerID)
|
||||
}
|
||||
if err := q.First(&agent).Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
return fn(tx, &human, &agent)
|
||||
})
|
||||
}
|
||||
|
||||
func restoreDigitalHumanAgent(tx *gorm.DB, scope types.DigitalHumanScope, binding *types.DigitalHumanBinding) error {
|
||||
var agent types.CustomAgent
|
||||
err := tx.First(&agent, "id = ? AND tenant_id = ? AND created_by = ?", binding.AgentID, scope.TenantID, binding.OwnerID).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if binding.AppliedVersion == 0 {
|
||||
return nil
|
||||
}
|
||||
agent.Config.SystemPrompt = removeDigitalHumanPrompt(agent.Config.SystemPrompt, binding.DigitalHumanID)
|
||||
return tx.Save(&agent).Error
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Delete(ctx context.Context, scope types.DigitalHumanScope, id string) error {
|
||||
_, err := r.DeleteWithIMChannels(ctx, scope, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) DeleteWithIMChannels(ctx context.Context, scope types.DigitalHumanScope, id string) ([]string, error) {
|
||||
var channelIDs []string
|
||||
err := r.mutate(ctx, scope, id, func(tx *gorm.DB, human *types.DigitalHuman) error {
|
||||
if human.ProcessingJobID != "" {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
var bindings []types.DigitalHumanBinding
|
||||
if err := tx.Where("tenant_id = ? AND digital_human_id = ?", scope.TenantID, id).Find(&bindings).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
if binding.AgentID != human.PrimaryAgentID {
|
||||
if err := restoreDigitalHumanAgent(tx, scope, &binding); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := tx.Where("id = ? AND tenant_id = ? AND created_by = ?", human.PrimaryAgentID, scope.TenantID, scope.OwnerID).Delete(&types.CustomAgent{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := detachFusionAgent(tx, human.PrimaryAgentID, scope.TenantID); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
channelIDs, err = softDeleteAgentIMChannels(tx, human.PrimaryAgentID, scope.TenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("tenant_id = ? AND digital_human_id = ?", scope.TenantID, id).Delete(&types.DigitalHumanBinding{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dhScope(tx, scope).Where("digital_human_id = ?", id).Delete(&types.DigitalHumanVersion{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := dhScope(tx, scope).Where("digital_human_id = ?", id).Delete(&types.DigitalHumanJob{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(human).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return channelIDs, nil
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Sources(ctx context.Context, scope types.DigitalHumanScope, id string, number int) (*types.DigitalHumanVersion, error) {
|
||||
var version types.DigitalHumanVersion
|
||||
err := dhScope(r.db.WithContext(ctx), scope).First(&version, "digital_human_id = ? AND number = ?", id, number).Error
|
||||
return &version, dhNotFound(err)
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) StartJob(ctx context.Context, scope types.DigitalHumanScope, id string, revision int64, job *types.DigitalHumanJob) error {
|
||||
return r.mutate(ctx, scope, id, func(tx *gorm.DB, human *types.DigitalHuman) error {
|
||||
if human.ProcessingJobID != "" || human.Revision != revision {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
if err := tx.Create(job).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
human.ProcessingJobID, human.Status = job.ID, "distilling"
|
||||
human.Revision++
|
||||
return tx.Save(human).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) Job(ctx context.Context, scope types.DigitalHumanScope, id, jobID string) (*types.DigitalHumanJob, error) {
|
||||
var job types.DigitalHumanJob
|
||||
err := dhReadScope(r.db.WithContext(ctx), scope).First(&job, "id = ? AND digital_human_id = ?", jobID, id).Error
|
||||
return &job, dhNotFound(err)
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) JobProgress(ctx context.Context, scope types.DigitalHumanScope, jobID string, progress int, stage string) error {
|
||||
result := dhScope(r.db.WithContext(ctx).Model(&types.DigitalHumanJob{}), scope).
|
||||
Where("id = ? AND status = ? AND expires_at > ?", jobID, "running", time.Now()).
|
||||
Updates(map[string]interface{}{"progress": progress, "stage": stage, "expires_at": time.Now().Add(types.DigitalHumanJobLease)})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) DistillCheckpoint(ctx context.Context, scope types.DigitalHumanScope, id, fingerprint string) (*types.DigitalHumanDistillCheckpoint, error) {
|
||||
var job types.DigitalHumanJob
|
||||
err := dhScope(r.db.WithContext(ctx), scope).
|
||||
Where("digital_human_id = ? AND checkpoint_fingerprint = ? AND status = ? AND checkpoint IS NOT NULL", id, fingerprint, "failed").
|
||||
Order("updated_at DESC, id DESC").Take(&job).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return job.Checkpoint, err
|
||||
}
|
||||
|
||||
// A checkpoint is private retry state. Locking the human fences superseded
|
||||
// workers without turning partially distilled evidence into a visible version.
|
||||
func (r *DigitalHumanRepository) SaveDistillCheckpoint(ctx context.Context, scope types.DigitalHumanScope, id, jobID, fingerprint string, checkpoint *types.DigitalHumanDistillCheckpoint) error {
|
||||
if len(fingerprint) != 64 || checkpoint == nil || checkpoint.CompletedBatches < 0 || checkpoint.CompletedBatches > checkpoint.TotalBatches || checkpoint.TotalBatches < 1 {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
encoded, err := json.Marshal(checkpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.mutate(ctx, scope, id, func(tx *gorm.DB, human *types.DigitalHuman) error {
|
||||
if human.ProcessingJobID != jobID {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
var job types.DigitalHumanJob
|
||||
if err := dhScope(tx, scope).Where("id = ? AND status = ? AND expires_at > ?", jobID, "running", time.Now()).Take(&job).Error; err != nil {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
if job.CheckpointFingerprint != "" && job.CheckpointFingerprint != fingerprint {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
if job.Checkpoint != nil && (job.Checkpoint.TotalBatches != checkpoint.TotalBatches || job.Checkpoint.CompletedBatches > checkpoint.CompletedBatches) {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
result := dhScope(tx.Model(&types.DigitalHumanJob{}), scope).
|
||||
Where("id = ? AND status = ? AND expires_at > ?", jobID, "running", time.Now()).Updates(map[string]interface{}{
|
||||
"checkpoint_fingerprint": fingerprint, "checkpoint": string(encoded), "expires_at": time.Now().Add(types.DigitalHumanJobLease),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *DigitalHumanRepository) FinishJob(ctx context.Context, scope types.DigitalHumanScope, id, jobID string, version *types.DigitalHumanVersion, failure string) error {
|
||||
return r.mutate(ctx, scope, id, func(tx *gorm.DB, human *types.DigitalHuman) error {
|
||||
if human.ProcessingJobID != jobID {
|
||||
return types.ErrDigitalHumanConflict
|
||||
}
|
||||
var job types.DigitalHumanJob
|
||||
if err := dhScope(tx, scope).First(&job, "id = ? AND status = ?", jobID, "running").Error; err != nil {
|
||||
return dhNotFound(err)
|
||||
}
|
||||
job.Status, job.Stage, job.Error = "failed", "蒸馏失败", failure
|
||||
if version != nil && time.Now().After(job.ExpiresAt) {
|
||||
version = nil
|
||||
job.Error = "蒸馏已超时,请重新提交。已审核版本保持可用。"
|
||||
}
|
||||
if version != nil {
|
||||
version.ID, version.TenantID, version.OwnerID, version.DigitalHumanID = uuid.NewString(), scope.TenantID, scope.OwnerID, id
|
||||
version.Number, version.Status = human.LatestVersion+1, "draft"
|
||||
if err := tx.Create(version).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
human.LatestVersion, human.Status = version.Number, "testing"
|
||||
job.Status, job.Progress, job.Stage, job.Version = "completed", 100, "蒸馏完成,等待人工审核", version.Number
|
||||
} else if human.ActiveVersion > 0 {
|
||||
human.Status = "active"
|
||||
} else {
|
||||
human.Status = "draft"
|
||||
}
|
||||
if err := tx.Save(&job).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
human.ProcessingJobID = ""
|
||||
human.Revision++
|
||||
return tx.Save(human).Error
|
||||
})
|
||||
}
|
||||
|
||||
// Recovery is scoped to readable humans but finalization still uses each actual owner.
|
||||
func (r *DigitalHumanRepository) RecoverExpiredJobs(ctx context.Context, scope types.DigitalHumanScope) error {
|
||||
var jobs []types.DigitalHumanJob
|
||||
if err := dhReadScope(r.db.WithContext(ctx), scope).Where("status = ? AND expires_at <= ?", "running", time.Now()).Limit(100).Find(&jobs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
ownerScope := types.DigitalHumanScope{TenantID: job.TenantID, OwnerID: job.OwnerID}
|
||||
err := r.FinishJob(ctx, ownerScope, job.DigitalHumanID, job.ID, nil, "蒸馏已超时或服务重启,请重新提交。已审核版本保持可用。")
|
||||
if err != nil && !errors.Is(err, types.ErrDigitalHumanConflict) && !errors.Is(err, types.ErrDigitalHumanNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user