chore(release): 版本发布 - 发布 v1.2.8.rc3 候选版本
修复生产前端 Rolldown 分包循环依赖白屏,补齐画布引用排序与素材 payload 合同,并同步工作流生成素材字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
01084c9a0f
commit
916043fb5a
21 files changed
+535
-37
No files matched your search
@@ -58,8 +58,8 @@ func (s *Service) UserAssetsPage(userID string, page int, pageSize int, filter U
|
||||
}
|
||||
rawAssets := make([]json.RawMessage, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
if strings.TrimSpace(asset.PayloadJSON) != "" {
|
||||
rawAssets = append(rawAssets, json.RawMessage(asset.PayloadJSON))
|
||||
if payload := clientAssetPayload(asset); len(payload) > 0 {
|
||||
rawAssets = append(rawAssets, payload)
|
||||
}
|
||||
}
|
||||
kindRows, categoryRows, folderRows, err := s.repo.UserAssetFacets(userID, filter.Status)
|
||||
|
||||
@@ -597,11 +597,35 @@ func (s *Service) ensureGeneratedProjectAsset(task model.Task, projectID string,
|
||||
title = "镜头产物"
|
||||
}
|
||||
title += " · " + label
|
||||
width := resource.Width
|
||||
height := resource.Height
|
||||
if width <= 0 {
|
||||
width = 1
|
||||
}
|
||||
if height <= 0 {
|
||||
height = 1
|
||||
}
|
||||
resourceURL := "/api/resources/" + resourceID + "/file"
|
||||
data := map[string]any{
|
||||
"storageKey": "resource:" + resourceID,
|
||||
"mimeType": resource.MimeType,
|
||||
"bytes": resource.Size,
|
||||
"width": width,
|
||||
"height": height,
|
||||
}
|
||||
if mediaType == "image" {
|
||||
data["dataUrl"] = resourceURL
|
||||
} else {
|
||||
data["url"] = resourceURL
|
||||
if resource.DurationMs > 0 {
|
||||
data["durationMs"] = resource.DurationMs
|
||||
}
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"id": assetID, "kind": mediaType, "category": model.AssetCategoryMaterial, "status": model.AssetVersionStatusConfirmed,
|
||||
"primaryVersionId": versionID, "title": title,
|
||||
"data": map[string]any{"storageKey": "resource:" + resourceID, "url": "/api/resources/" + resourceID + "/file", "mimeType": resource.MimeType, "bytes": resource.Size},
|
||||
"metadata": map[string]any{"source": "short-drama-workflow", "taskId": task.ID, "shotId": shot.ID, "projectIds": []string{projectID}},
|
||||
"primaryVersionId": versionID, "title": title, "coverUrl": resourceURL, "tags": []string{},
|
||||
"createdAt": now.UTC().Format(time.RFC3339Nano), "updatedAt": now.UTC().Format(time.RFC3339Nano),
|
||||
"data": data, "metadata": map[string]any{"source": "short-drama-workflow", "taskId": task.ID, "shotId": shot.ID, "projectIds": []string{projectID}},
|
||||
})
|
||||
asset := &model.Asset{ID: assetID, UserID: task.UserID, Kind: mediaType, Category: model.AssetCategoryMaterial, Status: model.AssetVersionStatusConfirmed, PrimaryVersionID: versionID, Title: title, PayloadJSON: string(payload), CreatedAt: now, UpdatedAt: now}
|
||||
version := &model.AssetVersion{ID: versionID, AssetID: assetID, Version: 1, Status: model.AssetVersionStatusConfirmed, DefinitionJSON: "{}", Prompt: task.Prompt, Note: "工作流生成产物", CreatedAt: now, UpdatedAt: now}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestClientAssetPayloadRepairsWorkflowAssetDocument(t *testing.T) {
|
||||
createdAt := time.Date(2026, 9, 9, 12, 0, 0, 0, time.UTC)
|
||||
asset := model.Asset{
|
||||
ID: "workflow-asset-test",
|
||||
PayloadJSON: `{
|
||||
"id":"workflow-asset-test",
|
||||
"kind":"video",
|
||||
"title":"镜头01 · 视频",
|
||||
"category":"material",
|
||||
"status":"confirmed",
|
||||
"tags":[],
|
||||
"data":{"url":"/api/resources/res-1/file","storageKey":"resource:res-1","width":0,"height":0,"bytes":1,"mimeType":"video/mp4"}
|
||||
}`,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
}
|
||||
|
||||
raw := clientAssetPayload(asset)
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
t.Fatalf("unmarshal payload: %v", err)
|
||||
}
|
||||
if payload["coverUrl"] != "/api/resources/res-1/file" {
|
||||
t.Fatalf("coverUrl = %#v", payload["coverUrl"])
|
||||
}
|
||||
if payload["createdAt"] == nil || payload["updatedAt"] == nil {
|
||||
t.Fatalf("timestamps missing: %#v", payload)
|
||||
}
|
||||
data := payload["data"].(map[string]any)
|
||||
if data["width"].(float64) != 1 || data["height"].(float64) != 1 {
|
||||
t.Fatalf("dimensions = %#v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientAssetPayloadPreservesCompleteDocument(t *testing.T) {
|
||||
rawInput := `{"id":"asset-1","kind":"image","title":"完整","coverUrl":"https://example.com/a.png","tags":["角色"],"createdAt":"2026-08-29T00:00:00.000Z","updatedAt":"2026-08-29T00:00:00.000Z","data":{"dataUrl":"https://example.com/a.png","width":2,"height":3,"bytes":1,"mimeType":"image/png"}}`
|
||||
asset := model.Asset{PayloadJSON: rawInput}
|
||||
raw := clientAssetPayload(asset)
|
||||
var before map[string]any
|
||||
var after map[string]any
|
||||
if err := json.Unmarshal([]byte(rawInput), &before); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(raw, &after); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
beforeJSON, _ := json.Marshal(before)
|
||||
afterJSON, _ := json.Marshal(after)
|
||||
if string(beforeJSON) != string(afterJSON) {
|
||||
t.Fatalf("payload changed: %s", raw)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func (s *Service) UserAsset(userID string, id string) (json.RawMessage, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.RawMessage(asset.PayloadJSON), nil
|
||||
return clientAssetPayload(*asset), nil
|
||||
}
|
||||
|
||||
func (s *Service) UpsertUserAsset(userID string, raw json.RawMessage) (UserDataSummary, error) {
|
||||
@@ -129,8 +129,8 @@ func (s *Service) UserAssets(userID string) ([]json.RawMessage, error) {
|
||||
}
|
||||
result := make([]json.RawMessage, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
if strings.TrimSpace(asset.PayloadJSON) != "" {
|
||||
result = append(result, json.RawMessage(asset.PayloadJSON))
|
||||
if payload := clientAssetPayload(asset); len(payload) > 0 {
|
||||
result = append(result, payload)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
||||
@@ -28,7 +28,9 @@ func (s *Service) UserAssetsByIDs(userID string, ids []string) ([]json.RawMessag
|
||||
}
|
||||
result := make([]json.RawMessage, 0, len(assets))
|
||||
for _, asset := range assets {
|
||||
result = append(result, json.RawMessage(asset.PayloadJSON))
|
||||
if payload := clientAssetPayload(asset); len(payload) > 0 {
|
||||
result = append(result, payload)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user