feat(seedance): 接入真人资产管理与引用
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
)
|
||||
|
||||
type PortraitAsset struct {
|
||||
ID string `json:"id"`
|
||||
GatewayUserID string `json:"gatewayUserId,omitempty"`
|
||||
UserID string `json:"userId"`
|
||||
GatewayTenantID string `json:"gatewayTenantId,omitempty"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
TenantKey string `json:"tenantKey,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
SourceType string `json:"sourceType"`
|
||||
URL string `json:"url"`
|
||||
Preview string `json:"preview,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
ByteSize int64 `json:"size,omitempty"`
|
||||
SourceSHA256 string `json:"sourceSha256,omitempty"`
|
||||
PrivateAvatarEligible bool `json:"privateAvatarEligible"`
|
||||
Status string `json:"status"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type PortraitAssetBinding struct {
|
||||
ID string `json:"id"`
|
||||
AssetID string `json:"assetId"`
|
||||
PlatformID string `json:"platformId"`
|
||||
ProjectName string `json:"projectName,omitempty"`
|
||||
AssetGroupID string `json:"assetGroupId,omitempty"`
|
||||
RemoteAssetID string `json:"remoteAssetId,omitempty"`
|
||||
RemoteAssetURI string `json:"remoteAssetUri,omitempty"`
|
||||
Status string `json:"status"`
|
||||
LastErrorCode string `json:"lastErrorCode,omitempty"`
|
||||
LastErrorMessage string `json:"lastErrorMessage,omitempty"`
|
||||
LastSyncedAt string `json:"lastSyncedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type PortraitAssetInput struct {
|
||||
GatewayUserID string
|
||||
UserID string
|
||||
GatewayTenantID string
|
||||
TenantID string
|
||||
TenantKey string
|
||||
Name string
|
||||
Description string
|
||||
SourceType string
|
||||
URL string
|
||||
Preview string
|
||||
MimeType string
|
||||
ByteSize int64
|
||||
SourceSHA256 string
|
||||
PrivateAvatarEligible bool
|
||||
Metadata map[string]any
|
||||
}
|
||||
|
||||
type PortraitAssetListFilter struct {
|
||||
Keyword string
|
||||
SourceType string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type PortraitAssetListResult struct {
|
||||
Items []PortraitAsset
|
||||
Total int
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type PortraitAssetPlatform struct {
|
||||
PlatformID string
|
||||
PlatformKey string
|
||||
Provider string
|
||||
Credentials map[string]any
|
||||
Config map[string]any
|
||||
}
|
||||
|
||||
const portraitAssetColumns = `
|
||||
a.id::text, COALESCE(a.gateway_user_id::text, ''), a.user_id,
|
||||
COALESCE(a.gateway_tenant_id::text, ''), COALESCE(a.tenant_id, ''), COALESCE(a.tenant_key, ''),
|
||||
a.name, a.description, a.source_type, a.url, a.preview, a.mime_type, a.byte_size,
|
||||
a.source_sha256, a.private_avatar_eligible, a.status, a.last_error, a.metadata, a.created_at, a.updated_at`
|
||||
|
||||
func (s *Store) CreatePortraitAsset(ctx context.Context, input PortraitAssetInput) (PortraitAsset, error) {
|
||||
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanPortraitAsset(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO gateway_portrait_assets (
|
||||
gateway_user_id, user_id, gateway_tenant_id, tenant_id, tenant_key,
|
||||
name, description, source_type, url, preview, mime_type, byte_size, source_sha256,
|
||||
private_avatar_eligible, status, metadata
|
||||
)
|
||||
VALUES (
|
||||
NULLIF($1, '')::uuid, $2, NULLIF($3, '')::uuid, NULLIF($4, ''), NULLIF($5, ''),
|
||||
$6, $7, $8, $9, $10, $11, $12, $13, $14, 'not_synced', $15::jsonb
|
||||
)
|
||||
RETURNING `+portraitAssetColumns,
|
||||
input.GatewayUserID, input.UserID, input.GatewayTenantID, input.TenantID, input.TenantKey,
|
||||
strings.TrimSpace(input.Name), strings.TrimSpace(input.Description), strings.TrimSpace(input.SourceType),
|
||||
strings.TrimSpace(input.URL), strings.TrimSpace(input.Preview), strings.TrimSpace(input.MimeType), input.ByteSize,
|
||||
strings.TrimSpace(input.SourceSHA256), input.PrivateAvatarEligible, string(metadata),
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) FindPortraitAssetBySourceHash(ctx context.Context, user *auth.User, sourceSHA256 string) (PortraitAsset, bool, error) {
|
||||
sourceSHA256 = strings.TrimSpace(sourceSHA256)
|
||||
if sourceSHA256 == "" {
|
||||
return PortraitAsset{}, false, nil
|
||||
}
|
||||
gatewayUserID, userID := portraitAssetUserKeys(user)
|
||||
asset, err := scanPortraitAsset(s.pool.QueryRow(ctx, `
|
||||
SELECT `+portraitAssetColumns+`
|
||||
FROM gateway_portrait_assets a
|
||||
WHERE ((NULLIF($1, '')::uuid IS NOT NULL AND a.gateway_user_id = NULLIF($1, '')::uuid)
|
||||
OR (NULLIF($2, '') IS NOT NULL AND a.user_id = $2))
|
||||
AND a.source_sha256 = $3
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 1`, gatewayUserID, userID, sourceSHA256))
|
||||
if IsNotFound(err) {
|
||||
return PortraitAsset{}, false, nil
|
||||
}
|
||||
return asset, err == nil, err
|
||||
}
|
||||
|
||||
func (s *Store) FindPortraitAssetForUser(ctx context.Context, user *auth.User, assetID string) (PortraitAsset, bool, error) {
|
||||
assetID = strings.TrimSpace(assetID)
|
||||
if assetID == "" {
|
||||
return PortraitAsset{}, false, nil
|
||||
}
|
||||
gatewayUserID, userID := portraitAssetUserKeys(user)
|
||||
asset, err := scanPortraitAsset(s.pool.QueryRow(ctx, `
|
||||
SELECT `+portraitAssetColumns+`
|
||||
FROM gateway_portrait_assets a
|
||||
WHERE a.id = NULLIF($3, '')::uuid
|
||||
AND ((NULLIF($1, '')::uuid IS NOT NULL AND a.gateway_user_id = NULLIF($1, '')::uuid)
|
||||
OR (NULLIF($2, '') IS NOT NULL AND a.user_id = $2))`, gatewayUserID, userID, assetID))
|
||||
if IsNotFound(err) {
|
||||
return PortraitAsset{}, false, nil
|
||||
}
|
||||
return asset, err == nil, err
|
||||
}
|
||||
|
||||
func (s *Store) ListPortraitAssets(ctx context.Context, user *auth.User, filter PortraitAssetListFilter) (PortraitAssetListResult, error) {
|
||||
page := filter.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := filter.PageSize
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
gatewayUserID, userID := portraitAssetUserKeys(user)
|
||||
keyword := strings.TrimSpace(filter.Keyword)
|
||||
if keyword != "" {
|
||||
keyword = "%" + keyword + "%"
|
||||
}
|
||||
where := `
|
||||
WHERE ((NULLIF($1, '')::uuid IS NOT NULL AND a.gateway_user_id = NULLIF($1, '')::uuid)
|
||||
OR (NULLIF($2, '') IS NOT NULL AND a.user_id = $2))
|
||||
AND (NULLIF($3, '') IS NULL OR a.source_type = $3)
|
||||
AND (NULLIF($4, '') IS NULL OR a.name ILIKE $4 OR a.description ILIKE $4)`
|
||||
args := []any{gatewayUserID, userID, strings.TrimSpace(filter.SourceType), keyword}
|
||||
var total int
|
||||
if err := s.pool.QueryRow(ctx, `SELECT count(*) FROM gateway_portrait_assets a `+where, args...).Scan(&total); err != nil {
|
||||
return PortraitAssetListResult{}, err
|
||||
}
|
||||
args = append(args, pageSize, (page-1)*pageSize)
|
||||
rows, err := s.pool.Query(ctx, `SELECT `+portraitAssetColumns+`
|
||||
FROM gateway_portrait_assets a `+where+`
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT $5 OFFSET $6`, args...)
|
||||
if err != nil {
|
||||
return PortraitAssetListResult{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]PortraitAsset, 0)
|
||||
for rows.Next() {
|
||||
asset, err := scanPortraitAsset(rows)
|
||||
if err != nil {
|
||||
return PortraitAssetListResult{}, err
|
||||
}
|
||||
items = append(items, asset)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return PortraitAssetListResult{}, err
|
||||
}
|
||||
return PortraitAssetListResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPortraitAssetBinding(ctx context.Context, assetID string, platformID string) (PortraitAssetBinding, bool, error) {
|
||||
binding, err := scanPortraitAssetBinding(s.pool.QueryRow(ctx, `
|
||||
SELECT id::text, asset_id::text, platform_id::text, project_name, asset_group_id,
|
||||
remote_asset_id, remote_asset_uri, status, last_error_code, last_error_message,
|
||||
COALESCE(last_synced_at::text, ''), created_at, updated_at
|
||||
FROM gateway_portrait_asset_bindings
|
||||
WHERE asset_id = $1::uuid AND platform_id = $2::uuid`, assetID, platformID))
|
||||
if IsNotFound(err) {
|
||||
return PortraitAssetBinding{}, false, nil
|
||||
}
|
||||
return binding, err == nil, err
|
||||
}
|
||||
|
||||
func (s *Store) UpsertPortraitAssetBinding(ctx context.Context, binding PortraitAssetBinding) (PortraitAssetBinding, error) {
|
||||
return scanPortraitAssetBinding(s.pool.QueryRow(ctx, `
|
||||
INSERT INTO gateway_portrait_asset_bindings (
|
||||
asset_id, platform_id, project_name, asset_group_id, remote_asset_id, remote_asset_uri,
|
||||
status, last_error_code, last_error_message, last_synced_at
|
||||
)
|
||||
VALUES ($1::uuid, $2::uuid, $3, $4, $5, $6, $7, $8, $9, now())
|
||||
ON CONFLICT (asset_id, platform_id) DO UPDATE SET
|
||||
project_name = EXCLUDED.project_name,
|
||||
asset_group_id = EXCLUDED.asset_group_id,
|
||||
remote_asset_id = CASE WHEN EXCLUDED.remote_asset_id <> '' THEN EXCLUDED.remote_asset_id ELSE gateway_portrait_asset_bindings.remote_asset_id END,
|
||||
remote_asset_uri = CASE WHEN EXCLUDED.remote_asset_uri <> '' THEN EXCLUDED.remote_asset_uri ELSE gateway_portrait_asset_bindings.remote_asset_uri END,
|
||||
status = EXCLUDED.status,
|
||||
last_error_code = EXCLUDED.last_error_code,
|
||||
last_error_message = EXCLUDED.last_error_message,
|
||||
last_synced_at = now(),
|
||||
updated_at = now()
|
||||
RETURNING id::text, asset_id::text, platform_id::text, project_name, asset_group_id,
|
||||
remote_asset_id, remote_asset_uri, status, last_error_code, last_error_message,
|
||||
COALESCE(last_synced_at::text, ''), created_at, updated_at`,
|
||||
binding.AssetID, binding.PlatformID, strings.TrimSpace(binding.ProjectName), strings.TrimSpace(binding.AssetGroupID),
|
||||
strings.TrimSpace(binding.RemoteAssetID), strings.TrimSpace(binding.RemoteAssetURI), strings.TrimSpace(binding.Status),
|
||||
strings.TrimSpace(binding.LastErrorCode), strings.TrimSpace(binding.LastErrorMessage),
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) UpdatePortraitAssetStatus(ctx context.Context, assetID string, status string, lastError string) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE gateway_portrait_assets
|
||||
SET status = $2, last_error = $3, updated_at = now()
|
||||
WHERE id = $1::uuid`, assetID, strings.TrimSpace(status), strings.TrimSpace(lastError))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) PortraitAssetBindingSummary(ctx context.Context, assetID string) (active int, total int, latestError string, updatedAt string, err error) {
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT COUNT(*) FILTER (WHERE status = 'active'), COUNT(*),
|
||||
COALESCE((ARRAY_AGG(NULLIF(last_error_message, '') ORDER BY updated_at DESC) FILTER (WHERE NULLIF(last_error_message, '') IS NOT NULL))[1], ''),
|
||||
COALESCE(MAX(updated_at)::text, '')
|
||||
FROM gateway_portrait_asset_bindings
|
||||
WHERE asset_id = $1::uuid`, assetID).Scan(&active, &total, &latestError, &updatedAt)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Store) ListPortraitAssetPlatforms(ctx context.Context) ([]PortraitAssetPlatform, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT p.id::text, p.platform_key, p.provider, p.credentials, p.config
|
||||
FROM integration_platforms p
|
||||
WHERE p.deleted_at IS NULL
|
||||
AND p.status = 'enabled'
|
||||
AND LOWER(p.provider) IN ('volces', 'volces-openai')
|
||||
ORDER BY COALESCE(p.dynamic_priority, p.priority), p.created_at`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]PortraitAssetPlatform, 0)
|
||||
for rows.Next() {
|
||||
var item PortraitAssetPlatform
|
||||
var credentials, config []byte
|
||||
if err := rows.Scan(&item.PlatformID, &item.PlatformKey, &item.Provider, &credentials, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Credentials = decodeObject(credentials)
|
||||
item.Config = decodeObject(config)
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func portraitAssetUserKeys(user *auth.User) (string, string) {
|
||||
if user == nil {
|
||||
return "", ""
|
||||
}
|
||||
gatewayUserID := strings.TrimSpace(user.GatewayUserID)
|
||||
if gatewayUserID == "" && user.Source == "gateway" {
|
||||
gatewayUserID = strings.TrimSpace(user.ID)
|
||||
}
|
||||
return gatewayUserID, strings.TrimSpace(user.ID)
|
||||
}
|
||||
|
||||
type portraitAssetScanner interface{ Scan(dest ...any) error }
|
||||
|
||||
func scanPortraitAsset(scanner portraitAssetScanner) (PortraitAsset, error) {
|
||||
var asset PortraitAsset
|
||||
var metadata []byte
|
||||
err := scanner.Scan(
|
||||
&asset.ID, &asset.GatewayUserID, &asset.UserID, &asset.GatewayTenantID, &asset.TenantID, &asset.TenantKey,
|
||||
&asset.Name, &asset.Description, &asset.SourceType, &asset.URL, &asset.Preview, &asset.MimeType, &asset.ByteSize,
|
||||
&asset.SourceSHA256, &asset.PrivateAvatarEligible, &asset.Status, &asset.LastError, &metadata, &asset.CreatedAt, &asset.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return PortraitAsset{}, err
|
||||
}
|
||||
asset.Metadata = decodeObject(metadata)
|
||||
return asset, nil
|
||||
}
|
||||
|
||||
func scanPortraitAssetBinding(scanner portraitAssetScanner) (PortraitAssetBinding, error) {
|
||||
var binding PortraitAssetBinding
|
||||
if err := scanner.Scan(
|
||||
&binding.ID, &binding.AssetID, &binding.PlatformID, &binding.ProjectName, &binding.AssetGroupID,
|
||||
&binding.RemoteAssetID, &binding.RemoteAssetURI, &binding.Status, &binding.LastErrorCode, &binding.LastErrorMessage,
|
||||
&binding.LastSyncedAt, &binding.CreatedAt, &binding.UpdatedAt,
|
||||
); err != nil {
|
||||
return PortraitAssetBinding{}, err
|
||||
}
|
||||
return binding, nil
|
||||
}
|
||||
Reference in New Issue
Block a user