feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复

This commit is contained in:
ddCat committed 2026-08-29 23:34:55 +08:00
1 parent 4f07daae9e
commit 2f6832f99c
113 files changed
+7016 -723

No files matched your search

@@ -463,6 +463,12 @@ func (s *Service) RegisterTaskOutputFromTask(task model.Task) error {
input.Role, _ = input.Metadata["role"].(string)
}
}
if strings.TrimSpace(input.ResourceID) == "" {
input.ResourceID, input.MediaType = taskOutputResource(task.ResultJSON, task.Type)
}
if strings.TrimSpace(input.MediaType) == "" && strings.TrimSpace(input.ResourceID) != "" {
input.MediaType = taskOutputMediaType(task.Type)
}
if strings.TrimSpace(input.WorkflowStepID) == "" {
return nil
}
@@ -475,10 +481,129 @@ func (s *Service) RegisterTaskOutputFromTask(task model.Task) error {
if projectID == "" {
return errors.New("任务未提供短剧项目 ID,无法登记产物")
}
if strings.TrimSpace(input.ResourceID) != "" && strings.TrimSpace(input.AssetVersionID) == "" && strings.TrimSpace(input.ShotID) != "" {
assetVersionID, assetErr := s.ensureGeneratedProjectAsset(task, projectID, input.ShotID, input.ResourceID, input.MediaType)
if assetErr != nil {
return assetErr
}
input.AssetVersionID = assetVersionID
}
_, err = s.RegisterTaskOutput(task.UserID, projectID, input.WorkflowStepID, RegisterTaskOutputRequest{TaskID: task.ID, CanvasID: input.CanvasID, UnitID: input.UnitID, ShotID: input.ShotID, ArtifactType: input.ArtifactType, AssetVersionID: input.AssetVersionID, ResourceID: input.ResourceID, MediaType: input.MediaType, Role: input.Role, OutputJSON: task.ResultJSON})
return err
}
func taskOutputMediaType(taskType string) string {
if strings.Contains(strings.ToLower(taskType), "video") {
return "video"
}
return "image"
}
func taskOutputResource(raw string, taskType string) (string, string) {
if strings.TrimSpace(raw) == "" {
return "", ""
}
var value any
if json.Unmarshal([]byte(raw), &value) != nil {
return "", ""
}
return findTaskOutputResource(value, taskOutputMediaType(taskType))
}
func findTaskOutputResource(value any, mediaType string) (string, string) {
switch item := value.(type) {
case []any:
for _, child := range item {
if id, kind := findTaskOutputResource(child, mediaType); id != "" {
return id, kind
}
}
case map[string]any:
for _, key := range []string{"resourceId", "storageKey", "url", "dataUrl", "resultUrl", "outputUrl"} {
if raw, ok := item[key].(string); ok {
text := strings.TrimSpace(raw)
if strings.HasPrefix(text, "resource:") {
return strings.TrimPrefix(text, "resource:"), mediaType
}
if strings.HasPrefix(text, "/api/resources/") {
id := strings.TrimPrefix(text, "/api/resources/")
if slash := strings.IndexByte(id, '/'); slash >= 0 {
id = id[:slash]
}
if id != "" {
return id, mediaType
}
}
}
}
for _, child := range item {
if id, kind := findTaskOutputResource(child, mediaType); id != "" {
return id, kind
}
}
}
return "", ""
}
func (s *Service) ensureGeneratedProjectAsset(task model.Task, projectID string, shotID string, resourceID string, mediaType string) (string, error) {
representations, err := s.repo.AssetRepresentationsForTask(task.ID)
if err != nil {
return "", err
}
for _, representation := range representations {
if representation.Role == "output" && representation.ResourceID == resourceID && representation.AssetVersionID != "" {
return representation.AssetVersionID, nil
}
}
resource, err := s.repo.ResourceForUser(task.UserID, resourceID)
if err != nil {
return "", err
}
if resource.Status != model.ResourceStatusReady {
return "", BadAuthRequest("生成资源尚未就绪,无法登记项目素材")
}
shot, err := s.repo.ShotForProject(projectID, shotID)
if err != nil {
return "", err
}
now := time.Now()
assetID := "workflow-asset-" + task.ID
versionID := "workflow-version-" + task.ID
label := "图片"
if mediaType == "video" {
label = "视频"
}
title := strings.TrimSpace(shot.Title)
if title == "" {
title = "镜头产物"
}
title += " · " + label
payload, _ := json.Marshal(map[string]any{
"id": assetID, "kind": mediaType, "category": model.AssetCategoryOther, "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}},
})
asset := &model.Asset{ID: assetID, UserID: task.UserID, Kind: mediaType, Category: model.AssetCategoryOther, 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}
folderID, err := s.resolveProjectAssetFolderID(projectID, nil)
if err != nil {
return "", err
}
position, err := s.repo.NextProjectAssetPosition(projectID, folderID)
if err != nil {
return "", err
}
link := &model.ProjectAssetLink{ID: newID(), ProjectID: projectID, AssetID: assetID, FolderID: folderID, Position: position, CreatedAt: now}
if err := s.repo.UpsertAsset(asset); err != nil {
return "", err
}
if _, err := s.repo.LinkProjectAsset(asset, version, link); err != nil {
return "", err
}
return versionID, nil
}
func validWorkflowStepStatus(status model.WorkflowStepStatus) bool {
switch status {
case model.WorkflowStepStatusPending, model.WorkflowStepStatusReady, model.WorkflowStepStatusRunning, model.WorkflowStepStatusReview, model.WorkflowStepStatusCompleted, model.WorkflowStepStatusFailed, model.WorkflowStepStatusSkipped: