Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_character.go

188 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"fmt"
"strings"
"yingce/backend/internal/model"
"yingce/backend/internal/repository"
)
type cloudAgentCharacter struct {
AssetID string
Name string
VersionPolicy string
Card CharacterCardSummary
}
func cloudAgentCharacterNode(node map[string]any) bool {
meta, _ := node["metadata"].(map[string]any)
return stringValue(node["type"]) == "text" && stringValue(meta["workflowKind"]) == "character"
}
func cloudAgentResolveCharacter(repo *repository.Repository, userID, projectID string, node map[string]any) (*cloudAgentCharacter, error) {
meta, _ := node["metadata"].(map[string]any)
assetID := stringValue(meta["characterAssetId"])
if assetID == "" {
return nil, BadAuthRequest("角色卡未关联角色资产")
}
_ = projectID
asset, err := repo.UserCharacterAsset(userID, assetID)
if err != nil {
return nil, BadAuthRequest("角色卡不存在或不可访问")
}
policy := stringValue(meta["characterVersionPolicy"])
versionID := asset.PrimaryVersionID
if policy == "pinned" {
versionID = stringValue(meta["characterVersionId"])
if versionID == "" {
return nil, BadAuthRequest("角色卡固定版本不存在")
}
} else if policy != "" && policy != "current" {
return nil, BadAuthRequest("角色卡版本策略无效")
} else {
policy = "current"
}
version, err := repo.AssetVersion(versionID)
if err != nil || version.AssetID != asset.ID {
return nil, BadAuthRequest("角色卡版本不存在或不属于当前角色")
}
card, err := (&Service{repo: repo}).characterCardVersion(userID, version)
if err != nil {
return nil, err
}
return &cloudAgentCharacter{AssetID: asset.ID, Name: asset.Title, VersionPolicy: policy, Card: card}, nil
}
var cloudAgentCharacterFields = []string{"role", "appearance", "physique", "clothing", "personality", "props", "consistencyPrompt", "multiViewPrompt", "voiceLanguage", "voiceAge", "voiceTimbre"}
func (character *cloudAgentCharacter) definition() map[string]any {
definition := map[string]any{}
for _, key := range cloudAgentCharacterFields {
if value, ok := character.Card.Definition[key].(string); ok && strings.TrimSpace(value) != "" {
definition[key] = truncateRunes(value, 4000)
}
}
if aliases, ok := character.Card.Definition["aliases"].([]any); ok {
values := []string{}
for _, item := range aliases[:min(len(aliases), 16)] {
if value, ok := item.(string); ok {
values = append(values, truncateRunes(value, 100))
}
}
if len(values) > 0 {
definition["aliases"] = values
}
}
return definition
}
func (character *cloudAgentCharacter) read(precise bool) map[string]any {
result := map[string]any{
"assetId": character.AssetID, "name": character.Name,
"versionId": character.Card.VersionID, "version": character.Card.Version,
"versionPolicy": character.VersionPolicy, "visualStatus": character.Card.VisualStatus,
"voiceStatus": character.Card.VoiceStatus, "hasDefinition": len(character.definition()) > 0,
}
if !precise {
return result
}
result["definition"] = character.definition()
roles := []string{}
representations := []map[string]any{}
for _, representation := range character.Card.Representations {
roles = append(roles, representation.Role)
representations = append(representations, map[string]any{
"id": representation.ID, "role": representation.Role, "mediaType": representation.MediaType,
"resourceId": representation.ResourceID,
})
}
result["representationRoles"] = roles
result["representations"] = representations
if character.Card.Voice != nil {
profile := character.Card.Voice.Profile
result["voice"] = map[string]any{"profileId": profile.ID, "name": profile.Name, "voiceKey": profile.VoiceKey, "language": profile.Language, "timbre": profile.Timbre, "sampleResourceId": profile.SampleResourceID, "instructions": truncateRunes(character.Card.Voice.Instructions, 1000)}
}
return result
}
func (character *cloudAgentCharacter) prompt() string {
parts := []string{}
for _, key := range []string{"role", "appearance", "physique", "clothing", "personality", "props", "consistencyPrompt"} {
if value, ok := character.Card.Definition[key].(string); ok && strings.TrimSpace(value) != "" {
parts = append(parts, strings.TrimSpace(value))
}
}
if len(parts) == 0 {
return ""
}
return fmt.Sprintf("【角色卡:%s(第%d版)】\n%s", character.Name, character.Card.Version, strings.Join(parts, "\n"))
}
func cloudAgentCharacterImageReference(repo *repository.Repository, userID, nodeID string, character *cloudAgentCharacter) (map[string]any, error) {
for _, role := range []string{"turnaround_sheet", "primary", "front"} {
for _, representation := range character.Card.Representations {
if representation.Role != role || !strings.HasPrefix(representation.MediaType, "image") || representation.ResourceID == "" {
continue
}
resource, err := repo.ResourceForUser(userID, representation.ResourceID)
if err != nil || resource.Status != model.ResourceStatusReady || !strings.HasPrefix(strings.ToLower(resource.MimeType), "image/") {
return nil, BadAuthRequest("角色卡三视图资源不存在、未就绪或不可访问")
}
return map[string]any{
"id": nodeID, "name": character.Name, "storageKey": "resource:" + resource.ID,
"canvasReferenceKind": "character",
"type": resource.MimeType, "mimeType": resource.MimeType, "bytes": resource.Size,
"width": resource.Width, "height": resource.Height, "inputKind": "image",
}, nil
}
}
return nil, BadAuthRequest("角色卡尚未绑定可用的三视图图片")
}
func cloudAgentCharacterAudioReference(repo *repository.Repository, userID, nodeID string, character *cloudAgentCharacter) (map[string]any, error) {
if character.Card.Voice == nil || strings.TrimSpace(character.Card.Voice.Profile.VoiceKey) == "" {
return nil, BadAuthRequest("角色卡尚未绑定可用的声音档案")
}
return map[string]any{
"id": nodeID, "name": character.Name, "voiceProfileId": character.Card.Voice.Profile.ID,
"voiceKey": character.Card.Voice.Profile.VoiceKey, "language": character.Card.Voice.Profile.Language,
"timbre": character.Card.Voice.Profile.Timbre, "instructions": character.Card.Voice.Instructions,
}, nil
}
func cloudAgentMediaReference(repo *repository.Repository, userID, projectID string, node map[string]any) (map[string]any, string, error) {
if !cloudAgentCharacterNode(node) {
return cloudAgentReference(repo, userID, node)
}
character, err := cloudAgentResolveCharacter(repo, userID, projectID, node)
if err != nil {
return nil, "", err
}
ref, err := cloudAgentCharacterImageReference(repo, userID, stringValue(node["id"]), character)
return ref, "referenceImages", err
}
func cloudAgentMediaCharacters(repo *repository.Repository, userID, projectID string, doc map[string]any, args cloudAgentMediaArgs) (map[string]*cloudAgentCharacter, error) {
nodes, err := creationObjects(doc["nodes"])
if err != nil {
return nil, err
}
characters := map[string]*cloudAgentCharacter{}
for _, id := range append(append([]string{}, args.ReferenceNodeIDs...), args.SourceNodeID) {
node := nodes[id]
if node == nil || !cloudAgentCharacterNode(node) {
continue
}
character, err := cloudAgentResolveCharacter(repo, userID, projectID, node)
if err != nil {
return nil, err
}
if id == args.SourceNodeID && character.prompt() == "" {
return nil, BadAuthRequest("角色卡还没有可用的文字设定,不能作为文本来源")
}
characters[id] = character
}
return characters, nil
}