154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
package canvas
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"time"
|
||
|
||
"yingce/backend/internal/model"
|
||
)
|
||
|
||
// clientAssetPayload 把数据库里的素材记录补齐为前端素材合同可消费的 JSON。
|
||
// 工作流等内部写入路径可能绕过 UpsertUserAsset,导致 payload 缺少 coverUrl、tags 或时间戳。
|
||
func ClientAssetPayload(asset model.Asset) json.RawMessage {
|
||
raw := strings.TrimSpace(asset.PayloadJSON)
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
var payload map[string]any
|
||
if err := json.Unmarshal([]byte(raw), &payload); err != nil || payload == nil {
|
||
return json.RawMessage(raw)
|
||
}
|
||
if _, ok := payload["coverUrl"]; !ok {
|
||
payload["coverUrl"] = deriveAssetCoverURL(payload)
|
||
}
|
||
if _, ok := payload["tags"]; !ok {
|
||
payload["tags"] = []string{}
|
||
}
|
||
if _, ok := payload["createdAt"]; !ok {
|
||
payload["createdAt"] = formatClientAssetTime(asset.CreatedAt)
|
||
}
|
||
if _, ok := payload["updatedAt"]; !ok {
|
||
payload["updatedAt"] = formatClientAssetTime(asset.UpdatedAt)
|
||
}
|
||
if data, ok := payload["data"].(map[string]any); ok {
|
||
kind, _ := payload["kind"].(string)
|
||
switch kind {
|
||
case "image", "video":
|
||
ensurePositiveAssetDimension(data, "width")
|
||
ensurePositiveAssetDimension(data, "height")
|
||
}
|
||
}
|
||
encoded, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return json.RawMessage(raw)
|
||
}
|
||
return encoded
|
||
}
|
||
|
||
// ClientAssetListPayload is the list variant used by the paginated asset
|
||
// library. The list only needs enough metadata to render cards and perform
|
||
// an action; generation prompts and other verbose metadata belong to the
|
||
// asset detail/snapshot paths. Keeping them out of every page also avoids
|
||
// repeatedly parsing and copying the same prompt strings in the browser.
|
||
func ClientAssetListPayload(asset model.Asset) json.RawMessage {
|
||
raw := ClientAssetPayload(asset)
|
||
if len(raw) == 0 {
|
||
return nil
|
||
}
|
||
var payload map[string]any
|
||
if err := json.Unmarshal(raw, &payload); err != nil || payload == nil {
|
||
return raw
|
||
}
|
||
if metadata, ok := payload["metadata"].(map[string]any); ok {
|
||
// 列表只需要项目归属提示。canvasId 仍保留,避免列表卡片把画布素材
|
||
// 错显示成“未关联项目”;节点 ID、生成提示和其他同步上下文在详情路径读取。
|
||
for key := range metadata {
|
||
if key != "projectName" && key != "projectIds" && key != "canvasId" {
|
||
delete(metadata, key)
|
||
}
|
||
}
|
||
if len(metadata) == 0 {
|
||
delete(payload, "metadata")
|
||
}
|
||
}
|
||
// 图片封面和 dataUrl、视频封面和 url 通常是同一个地址;列表保留
|
||
// coverUrl 字段以满足前端合同,但不再重复传输这段长 URL。
|
||
if cover, ok := payload["coverUrl"].(string); ok {
|
||
if data, ok := payload["data"].(map[string]any); ok {
|
||
for _, key := range []string{"dataUrl", "url"} {
|
||
if value, ok := data[key].(string); ok && value == cover {
|
||
payload["coverUrl"] = ""
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
encoded, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return raw
|
||
}
|
||
return encoded
|
||
}
|
||
|
||
func deriveAssetCoverURL(payload map[string]any) string {
|
||
if value, ok := payload["coverUrl"].(string); ok && strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
data, ok := payload["data"].(map[string]any)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
for _, key := range []string{"dataUrl", "url"} {
|
||
if value, ok := data[key].(string); ok && strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
}
|
||
if storageKey, ok := data["storageKey"].(string); ok {
|
||
return resourceURLFromStorageKey(storageKey)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func resourceURLFromStorageKey(storageKey string) string {
|
||
storageKey = strings.TrimSpace(storageKey)
|
||
if !strings.HasPrefix(storageKey, "resource:") {
|
||
return ""
|
||
}
|
||
resourceID := strings.TrimSpace(strings.TrimPrefix(storageKey, "resource:"))
|
||
if resourceID == "" {
|
||
return ""
|
||
}
|
||
return "/api/resources/" + resourceID + "/file"
|
||
}
|
||
|
||
func ensurePositiveAssetDimension(data map[string]any, key string) {
|
||
value, ok := jsonNumberValue(data[key])
|
||
if !ok || value <= 0 {
|
||
data[key] = 1
|
||
}
|
||
}
|
||
|
||
func jsonNumberValue(value any) (float64, bool) {
|
||
switch item := value.(type) {
|
||
case float64:
|
||
return item, true
|
||
case int:
|
||
return float64(item), true
|
||
case int64:
|
||
return float64(item), true
|
||
case json.Number:
|
||
parsed, err := item.Float64()
|
||
return parsed, err == nil
|
||
default:
|
||
return 0, false
|
||
}
|
||
}
|
||
|
||
func formatClientAssetTime(value time.Time) string {
|
||
if value.IsZero() {
|
||
return time.Now().UTC().Format(time.RFC3339Nano)
|
||
}
|
||
return value.UTC().Format(time.RFC3339Nano)
|
||
}
|