提交昨晚更新改动
This commit is contained in:
@@ -9,10 +9,11 @@ import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceListRequest
|
|||||||
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceSynthesisRequest;
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceSynthesisRequest;
|
||||||
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoWorkflowResultRequest;
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoWorkflowResultRequest;
|
||||||
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoWorkflowRunRequest;
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoWorkflowRunRequest;
|
||||||
import com.nanri.aiimage.modules.imagevideo.model.vo.DouyinCopyVo;
|
import com.nanri.aiimage.modules.imagevideo.model.vo.ImageVideoAsyncTaskVo;
|
||||||
import com.nanri.aiimage.modules.imagevideo.model.vo.ImageVideoMediaUploadVo;
|
import com.nanri.aiimage.modules.imagevideo.model.vo.ImageVideoMediaUploadVo;
|
||||||
import com.nanri.aiimage.modules.imagevideo.model.vo.ImageVideoSecretStatusVo;
|
import com.nanri.aiimage.modules.imagevideo.model.vo.ImageVideoSecretStatusVo;
|
||||||
import com.nanri.aiimage.modules.imagevideo.service.ImageVideoCozeService;
|
import com.nanri.aiimage.modules.imagevideo.service.ImageVideoCozeService;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.service.ImageVideoAsyncTaskService;
|
||||||
import com.nanri.aiimage.modules.imagevideo.service.ImageVideoSecretService;
|
import com.nanri.aiimage.modules.imagevideo.service.ImageVideoSecretService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -20,6 +21,7 @@ import jakarta.validation.Valid;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -27,8 +29,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/api/image-video")
|
@RequestMapping("/api/image-video")
|
||||||
@@ -37,6 +37,7 @@ public class ImageVideoController {
|
|||||||
|
|
||||||
private final ImageVideoCozeService imageVideoCozeService;
|
private final ImageVideoCozeService imageVideoCozeService;
|
||||||
private final ImageVideoSecretService imageVideoSecretService;
|
private final ImageVideoSecretService imageVideoSecretService;
|
||||||
|
private final ImageVideoAsyncTaskService imageVideoAsyncTaskService;
|
||||||
|
|
||||||
@GetMapping("/secrets")
|
@GetMapping("/secrets")
|
||||||
@Operation(summary = "查询视频密钥配置状态")
|
@Operation(summary = "查询视频密钥配置状态")
|
||||||
@@ -52,20 +53,20 @@ public class ImageVideoController {
|
|||||||
|
|
||||||
@PostMapping("/douyin-copy")
|
@PostMapping("/douyin-copy")
|
||||||
@Operation(summary = "抖音文案识别和仿写")
|
@Operation(summary = "抖音文案识别和仿写")
|
||||||
public ApiResponse<DouyinCopyVo> douyinCopy(@Valid @RequestBody DouyinCopyRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> douyinCopy(@Valid @RequestBody DouyinCopyRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.runDouyinCopy(request));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitDouyinCopy(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/workflow/run")
|
@PostMapping("/workflow/run")
|
||||||
@Operation(summary = "提交图生/复刻视频工作流")
|
@Operation(summary = "提交图生/复刻视频工作流")
|
||||||
public ApiResponse<Map<String, Object>> runWorkflow(@Valid @RequestBody ImageVideoWorkflowRunRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> runWorkflow(@Valid @RequestBody ImageVideoWorkflowRunRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.runImageVideoWorkflow(request.getUserId(), request.getParameters()));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitWorkflow(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/workflow/result")
|
@PostMapping("/workflow/result")
|
||||||
@Operation(summary = "查询图生/复刻视频工作流异步结果")
|
@Operation(summary = "查询图生/复刻视频工作流异步结果")
|
||||||
public ApiResponse<Map<String, Object>> workflowResult(@Valid @RequestBody ImageVideoWorkflowResultRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> workflowResult(@Valid @RequestBody ImageVideoWorkflowResultRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.getImageVideoWorkflowResult(request.getUserId(), request.getExecuteId()));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitWorkflowResult(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/media/upload")
|
@PostMapping("/media/upload")
|
||||||
@@ -76,25 +77,33 @@ public class ImageVideoController {
|
|||||||
|
|
||||||
@PostMapping("/voice/list")
|
@PostMapping("/voice/list")
|
||||||
@Operation(summary = "查看音色")
|
@Operation(summary = "查看音色")
|
||||||
public ApiResponse<Map<String, Object>> listVoices(@Valid @RequestBody ImageVideoVoiceListRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> listVoices(@Valid @RequestBody ImageVideoVoiceListRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.runVoiceList(request.getUserId(), request.getName()));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitVoiceList(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/voice/delete")
|
@PostMapping("/voice/delete")
|
||||||
@Operation(summary = "删除音色")
|
@Operation(summary = "删除音色")
|
||||||
public ApiResponse<Map<String, Object>> deleteVoice(@Valid @RequestBody ImageVideoVoiceDeleteRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> deleteVoice(@Valid @RequestBody ImageVideoVoiceDeleteRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.runVoiceDelete(request.getUserId(), request.getVoiceId()));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitVoiceDelete(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/voice/clone")
|
@PostMapping("/voice/clone")
|
||||||
@Operation(summary = "音色克隆")
|
@Operation(summary = "音色克隆")
|
||||||
public ApiResponse<Map<String, Object>> cloneVoice(@Valid @RequestBody ImageVideoVoiceCloneRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> cloneVoice(@Valid @RequestBody ImageVideoVoiceCloneRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.runVoiceClone(request.getUserId(), request.getName(), request.getAudioUrl(), request.getVideoUrl()));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitVoiceClone(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/voice/synthesis")
|
@PostMapping("/voice/synthesis")
|
||||||
@Operation(summary = "语音合成")
|
@Operation(summary = "语音合成")
|
||||||
public ApiResponse<Map<String, Object>> synthesizeVoice(@Valid @RequestBody ImageVideoVoiceSynthesisRequest request) {
|
public ApiResponse<ImageVideoAsyncTaskVo> synthesizeVoice(@Valid @RequestBody ImageVideoVoiceSynthesisRequest request) {
|
||||||
return ApiResponse.success(imageVideoCozeService.runVoiceSynthesis(request.getUserId(), request.getText(), request.getVoiceId()));
|
return ApiResponse.success(imageVideoAsyncTaskService.submitVoiceSynthesis(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks/{taskId}")
|
||||||
|
@Operation(summary = "Query image video async task")
|
||||||
|
public ApiResponse<ImageVideoAsyncTaskVo> getTask(
|
||||||
|
@PathVariable Long taskId,
|
||||||
|
@RequestParam("user_id") Long userId) {
|
||||||
|
return ApiResponse.success(imageVideoAsyncTaskService.getTask(taskId, userId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.nanri.aiimage.modules.imagevideo.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.entity.ImageVideoAsyncTaskEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface ImageVideoAsyncTaskMapper extends BaseMapper<ImageVideoAsyncTaskEntity> {
|
||||||
|
|
||||||
|
@Update("UPDATE biz_image_video_async_task "
|
||||||
|
+ "SET status = 'RUNNING', updated_at = NOW(), attempt_count = attempt_count + 1 "
|
||||||
|
+ "WHERE id = #{taskId} AND status = 'PENDING'")
|
||||||
|
int claimPending(Long taskId);
|
||||||
|
|
||||||
|
@Update("UPDATE biz_image_video_async_task "
|
||||||
|
+ "SET status = 'POLLING', updated_at = NOW(), attempt_count = attempt_count + 1 "
|
||||||
|
+ "WHERE id = #{taskId} AND status = 'WAITING'")
|
||||||
|
int claimWaiting(Long taskId);
|
||||||
|
}
|
||||||
@@ -12,18 +12,21 @@ public class ImageVideoSecretSaveRequest {
|
|||||||
@Schema(description = "用户ID", example = "1")
|
@Schema(description = "用户ID", example = "1")
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
@Schema(description = "文案工作流 api_key;首次配置或过期后必填", example = "sk-...")
|
@Schema(description = "爬虫密钥;首次配置或过期后必填", example = "sk-...")
|
||||||
private String copyApiKey;
|
private String copyApiKey;
|
||||||
|
|
||||||
private String t8Key;
|
private String t8Key;
|
||||||
|
|
||||||
@Schema(description = "音色工作流 api_key;首次配置或过期后必填", example = "sk-api-...")
|
@Schema(description = "T8密钥(国内);首次配置或过期后必填")
|
||||||
|
private String t8VideoKey;
|
||||||
|
|
||||||
|
@Schema(description = "音频密钥;首次配置或过期后必填", example = "sk-api-...")
|
||||||
private String voiceApiKey;
|
private String voiceApiKey;
|
||||||
|
|
||||||
@Schema(description = "音色 group_id;首次配置或过期后必填", example = "2030928714635682107")
|
@Schema(description = "音频团队ID;首次配置或过期后必填", example = "2030928714635682107")
|
||||||
private String voiceGroupId;
|
private String voiceGroupId;
|
||||||
|
|
||||||
@NotNull(message = "有效期不能为空")
|
@NotNull(message = "有效期不能为空")
|
||||||
@Schema(description = "有效期天数,只允许 1、7、30", example = "7")
|
@Schema(description = "有效期天数,只允许 1、7、30、0;0 表示永久", example = "7")
|
||||||
private Integer expireDays;
|
private Integer expireDays;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.nanri.aiimage.modules.imagevideo.model.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@TableName("biz_image_video_async_task")
|
||||||
|
public class ImageVideoAsyncTaskEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long userId;
|
||||||
|
private String taskType;
|
||||||
|
private String status;
|
||||||
|
private String requestJson;
|
||||||
|
private String resultJson;
|
||||||
|
private String errorMessage;
|
||||||
|
private String cozeExecuteId;
|
||||||
|
private String cozeStatus;
|
||||||
|
private Integer attemptCount;
|
||||||
|
private LocalDateTime submittedAt;
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public class ImageVideoSecretEntity {
|
|||||||
private Long userId;
|
private Long userId;
|
||||||
private String copyApiKey;
|
private String copyApiKey;
|
||||||
private String t8Key;
|
private String t8Key;
|
||||||
|
private String t8VideoKey;
|
||||||
private String voiceApiKey;
|
private String voiceApiKey;
|
||||||
private String voiceGroupId;
|
private String voiceGroupId;
|
||||||
private LocalDateTime expiresAt;
|
private LocalDateTime expiresAt;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.nanri.aiimage.modules.imagevideo.model.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ImageVideoAsyncTaskVo {
|
||||||
|
private Long taskId;
|
||||||
|
private String taskType;
|
||||||
|
private String status;
|
||||||
|
private String cozeExecuteId;
|
||||||
|
private String cozeStatus;
|
||||||
|
private Object result;
|
||||||
|
private String errorMessage;
|
||||||
|
private LocalDateTime submittedAt;
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
}
|
||||||
@@ -21,29 +21,33 @@ public class ImageVideoSecretStatusVo {
|
|||||||
@Schema(description = "是否已经过期")
|
@Schema(description = "是否已经过期")
|
||||||
private Boolean expired;
|
private Boolean expired;
|
||||||
|
|
||||||
@Schema(description = "文案 key 是否已保存")
|
@Schema(description = "爬虫密钥是否已保存")
|
||||||
private Boolean hasCopyApiKey;
|
private Boolean hasCopyApiKey;
|
||||||
|
|
||||||
private Boolean hasT8Key;
|
private Boolean hasT8Key;
|
||||||
|
|
||||||
@Schema(description = "音色 key 是否已保存")
|
private Boolean hasT8VideoKey;
|
||||||
|
|
||||||
|
@Schema(description = "音频密钥是否已保存")
|
||||||
private Boolean hasVoiceApiKey;
|
private Boolean hasVoiceApiKey;
|
||||||
|
|
||||||
@Schema(description = "音色 group_id 是否已保存")
|
@Schema(description = "音频团队ID是否已保存")
|
||||||
private Boolean hasVoiceGroupId;
|
private Boolean hasVoiceGroupId;
|
||||||
|
|
||||||
@Schema(description = "脱敏后的文案 key")
|
@Schema(description = "脱敏后的爬虫密钥")
|
||||||
private String copyApiKeyMasked;
|
private String copyApiKeyMasked;
|
||||||
|
|
||||||
private String t8KeyMasked;
|
private String t8KeyMasked;
|
||||||
|
|
||||||
@Schema(description = "脱敏后的音色 key")
|
private String t8VideoKeyMasked;
|
||||||
|
|
||||||
|
@Schema(description = "脱敏后的音频密钥")
|
||||||
private String voiceApiKeyMasked;
|
private String voiceApiKeyMasked;
|
||||||
|
|
||||||
@Schema(description = "脱敏后的音色 group_id")
|
@Schema(description = "脱敏后的音频团队ID")
|
||||||
private String voiceGroupIdMasked;
|
private String voiceGroupIdMasked;
|
||||||
|
|
||||||
@Schema(description = "用户选择的有效期天数,支持 1、7、30")
|
@Schema(description = "用户选择的有效期天数,支持 1、7、30、0;0 表示永久")
|
||||||
private Integer expireDays;
|
private Integer expireDays;
|
||||||
|
|
||||||
@Schema(description = "过期时间")
|
@Schema(description = "过期时间")
|
||||||
|
|||||||
@@ -0,0 +1,475 @@
|
|||||||
|
package com.nanri.aiimage.modules.imagevideo.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.mapper.ImageVideoAsyncTaskMapper;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.DouyinCopyRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceCloneRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceDeleteRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceListRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoVoiceSynthesisRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoWorkflowResultRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.dto.ImageVideoWorkflowRunRequest;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.entity.ImageVideoAsyncTaskEntity;
|
||||||
|
import com.nanri.aiimage.modules.imagevideo.model.vo.ImageVideoAsyncTaskVo;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.core.task.TaskExecutor;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ImageVideoAsyncTaskService {
|
||||||
|
|
||||||
|
private static final int DISPATCH_BATCH_SIZE = 20;
|
||||||
|
private static final int POLL_BATCH_SIZE = 50;
|
||||||
|
private static final Set<String> TERMINAL_STATUSES = Set.of(
|
||||||
|
"SUCCESS", "SUCCEEDED", "COMPLETED", "DONE", "FINISHED",
|
||||||
|
"FAILED", "FAIL", "ERROR", "CANCELED", "CANCELLED"
|
||||||
|
);
|
||||||
|
private static final Set<String> FAILED_STATUSES = Set.of("FAILED", "FAIL", "ERROR", "CANCELED", "CANCELLED");
|
||||||
|
private static final Set<String> COZE_EXECUTION_STATUS_FIELDS = Set.of(
|
||||||
|
"execute_status", "executeStatus", "workflow_status", "workflowStatus", "status"
|
||||||
|
);
|
||||||
|
private static final Set<String> COZE_PRIMARY_STATUS_FIELDS = Set.of(
|
||||||
|
"execute_status", "executeStatus", "workflow_status", "workflowStatus"
|
||||||
|
);
|
||||||
|
|
||||||
|
private final ImageVideoAsyncTaskMapper taskMapper;
|
||||||
|
private final ImageVideoCozeService cozeService;
|
||||||
|
private final ImageVideoWorkflowConfigService workflowConfigService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final TaskExecutor cozeTaskExecutor;
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskService(
|
||||||
|
ImageVideoAsyncTaskMapper taskMapper,
|
||||||
|
ImageVideoCozeService cozeService,
|
||||||
|
ImageVideoWorkflowConfigService workflowConfigService,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
@Qualifier("cozeTaskExecutor") TaskExecutor cozeTaskExecutor) {
|
||||||
|
this.taskMapper = taskMapper;
|
||||||
|
this.cozeService = cozeService;
|
||||||
|
this.workflowConfigService = workflowConfigService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.cozeTaskExecutor = cozeTaskExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitDouyinCopy(DouyinCopyRequest request) {
|
||||||
|
return submit(TaskType.DOUYIN_COPY, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitWorkflow(ImageVideoWorkflowRunRequest request) {
|
||||||
|
return submit(TaskType.IMAGE_VIDEO_WORKFLOW, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitWorkflowResult(ImageVideoWorkflowResultRequest request) {
|
||||||
|
return submit(TaskType.WORKFLOW_RESULT, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitVoiceList(ImageVideoVoiceListRequest request) {
|
||||||
|
return submit(TaskType.VOICE_LIST, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitVoiceDelete(ImageVideoVoiceDeleteRequest request) {
|
||||||
|
return submit(TaskType.VOICE_DELETE, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitVoiceClone(ImageVideoVoiceCloneRequest request) {
|
||||||
|
return submit(TaskType.VOICE_CLONE, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo submitVoiceSynthesis(ImageVideoVoiceSynthesisRequest request) {
|
||||||
|
return submit(TaskType.VOICE_SYNTHESIS, request.getUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageVideoAsyncTaskVo getTask(Long taskId, Long userId) {
|
||||||
|
ImageVideoAsyncTaskEntity task = taskMapper.selectOne(new LambdaQueryWrapper<ImageVideoAsyncTaskEntity>()
|
||||||
|
.eq(ImageVideoAsyncTaskEntity::getId, taskId)
|
||||||
|
.eq(ImageVideoAsyncTaskEntity::getUserId, userId));
|
||||||
|
if (task == null) {
|
||||||
|
throw new BusinessException("Image video task not found");
|
||||||
|
}
|
||||||
|
task = recoverFalseFailedTask(task);
|
||||||
|
return toVo(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedDelayString = "${aiimage.image-video.async-task-dispatch-delay-ms:1000}")
|
||||||
|
public void dispatchPendingTasks() {
|
||||||
|
List<ImageVideoAsyncTaskEntity> tasks = taskMapper.selectList(new LambdaQueryWrapper<ImageVideoAsyncTaskEntity>()
|
||||||
|
.eq(ImageVideoAsyncTaskEntity::getStatus, TaskStatus.PENDING.name())
|
||||||
|
.orderByAsc(ImageVideoAsyncTaskEntity::getId)
|
||||||
|
.last("LIMIT " + DISPATCH_BATCH_SIZE));
|
||||||
|
tasks.forEach(task -> cozeTaskExecutor.execute(() -> executeTask(task.getId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedDelayString = "${aiimage.image-video.async-task-poll-delay-ms:5000}")
|
||||||
|
public void pollWaitingTasks() {
|
||||||
|
List<ImageVideoAsyncTaskEntity> tasks = taskMapper.selectList(new LambdaQueryWrapper<ImageVideoAsyncTaskEntity>()
|
||||||
|
.eq(ImageVideoAsyncTaskEntity::getStatus, TaskStatus.WAITING.name())
|
||||||
|
.orderByAsc(ImageVideoAsyncTaskEntity::getUpdatedAt)
|
||||||
|
.last("LIMIT " + POLL_BATCH_SIZE));
|
||||||
|
tasks.forEach(task -> cozeTaskExecutor.execute(() -> pollTask(task.getId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageVideoAsyncTaskVo submit(TaskType type, Long userId, Object payload) {
|
||||||
|
if (userId == null || userId <= 0) {
|
||||||
|
throw new BusinessException("userId is required");
|
||||||
|
}
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
ImageVideoAsyncTaskEntity task = new ImageVideoAsyncTaskEntity();
|
||||||
|
task.setUserId(userId);
|
||||||
|
task.setTaskType(type.name());
|
||||||
|
task.setStatus(TaskStatus.PENDING.name());
|
||||||
|
task.setRequestJson(writeJson(payload));
|
||||||
|
task.setAttemptCount(0);
|
||||||
|
task.setSubmittedAt(now);
|
||||||
|
task.setCreatedAt(now);
|
||||||
|
task.setUpdatedAt(now);
|
||||||
|
taskMapper.insert(task);
|
||||||
|
cozeTaskExecutor.execute(() -> executeTask(task.getId()));
|
||||||
|
return toVo(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeTask(Long taskId) {
|
||||||
|
if (taskMapper.claimPending(taskId) != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImageVideoAsyncTaskEntity task = taskMapper.selectById(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TaskType type = TaskType.valueOf(task.getTaskType());
|
||||||
|
if (type == TaskType.WORKFLOW_RESULT) {
|
||||||
|
ImageVideoWorkflowResultRequest request = readJson(task.getRequestJson(), ImageVideoWorkflowResultRequest.class);
|
||||||
|
waitForCoze(task, request.getExecuteId(), "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> response = submitToCoze(type, task.getRequestJson());
|
||||||
|
String executeId = findText(response, Set.of("execute_id", "executeId"));
|
||||||
|
String cozeStatus = resolveCozeExecutionStatus(response);
|
||||||
|
if (!executeId.isBlank() && !TERMINAL_STATUSES.contains(cozeStatus)) {
|
||||||
|
waitForCoze(task, executeId, cozeStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completeTask(task, transformResult(type, response), cozeStatus);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
failTask(task, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollTask(Long taskId) {
|
||||||
|
if (taskMapper.claimWaiting(taskId) != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImageVideoAsyncTaskEntity task = taskMapper.selectById(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task.getSubmittedAt() != null && task.getSubmittedAt().plusHours(1).isBefore(LocalDateTime.now())) {
|
||||||
|
failTask(task, new BusinessException("Coze task exceeded the one-hour polling limit"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TaskType type = TaskType.valueOf(task.getTaskType());
|
||||||
|
String workflowId = workflowIdFor(type);
|
||||||
|
Map<String, Object> result = cozeService.getWorkflowResult(task.getUserId(), workflowId, task.getCozeExecuteId());
|
||||||
|
String cozeStatus = resolveCozeExecutionStatus(result);
|
||||||
|
if (TERMINAL_STATUSES.contains(cozeStatus)) {
|
||||||
|
if (FAILED_STATUSES.contains(cozeStatus)) {
|
||||||
|
task.setCozeStatus(cozeStatus);
|
||||||
|
failTask(task, new BusinessException("Coze workflow finished with status " + cozeStatus));
|
||||||
|
} else {
|
||||||
|
completeTask(task, transformResult(type, result), cozeStatus);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
task.setStatus(TaskStatus.WAITING.name());
|
||||||
|
task.setCozeStatus(cozeStatus);
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
taskMapper.updateById(task);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Coze history calls are retried by the next polling cycle until the task's overall deadline.
|
||||||
|
task.setStatus(TaskStatus.WAITING.name());
|
||||||
|
task.setErrorMessage(truncate(messageOf(ex)));
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
taskMapper.updateById(task);
|
||||||
|
log.warn("[image-video] async task poll failed taskId={} type={} error={}",
|
||||||
|
task.getId(), task.getTaskType(), messageOf(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> submitToCoze(TaskType type, String requestJson) {
|
||||||
|
return switch (type) {
|
||||||
|
case DOUYIN_COPY -> cozeService.submitDouyinCopy(readJson(requestJson, DouyinCopyRequest.class));
|
||||||
|
case IMAGE_VIDEO_WORKFLOW -> {
|
||||||
|
ImageVideoWorkflowRunRequest request = readJson(requestJson, ImageVideoWorkflowRunRequest.class);
|
||||||
|
yield cozeService.runImageVideoWorkflow(request.getUserId(), request.getParameters());
|
||||||
|
}
|
||||||
|
case VOICE_LIST -> {
|
||||||
|
ImageVideoVoiceListRequest request = readJson(requestJson, ImageVideoVoiceListRequest.class);
|
||||||
|
yield cozeService.runVoiceList(request.getUserId(), request.getName());
|
||||||
|
}
|
||||||
|
case VOICE_DELETE -> {
|
||||||
|
ImageVideoVoiceDeleteRequest request = readJson(requestJson, ImageVideoVoiceDeleteRequest.class);
|
||||||
|
yield cozeService.runVoiceDelete(request.getUserId(), request.getVoiceId());
|
||||||
|
}
|
||||||
|
case VOICE_CLONE -> {
|
||||||
|
ImageVideoVoiceCloneRequest request = readJson(requestJson, ImageVideoVoiceCloneRequest.class);
|
||||||
|
yield cozeService.runVoiceClone(request.getUserId(), request.getName(), request.getAudioUrl(), request.getVideoUrl());
|
||||||
|
}
|
||||||
|
case VOICE_SYNTHESIS -> {
|
||||||
|
ImageVideoVoiceSynthesisRequest request = readJson(requestJson, ImageVideoVoiceSynthesisRequest.class);
|
||||||
|
yield cozeService.runVoiceSynthesis(request.getUserId(), request.getText(), request.getVoiceId());
|
||||||
|
}
|
||||||
|
case WORKFLOW_RESULT -> throw new IllegalStateException("Workflow result tasks do not submit a workflow");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String workflowIdFor(TaskType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case DOUYIN_COPY -> workflowConfigService.douyinCopyWorkflowId();
|
||||||
|
case IMAGE_VIDEO_WORKFLOW, WORKFLOW_RESULT -> workflowConfigService.imageVideoWorkflowId();
|
||||||
|
case VOICE_LIST -> workflowConfigService.voiceListWorkflowId();
|
||||||
|
case VOICE_DELETE -> workflowConfigService.voiceDeleteWorkflowId();
|
||||||
|
case VOICE_CLONE -> workflowConfigService.voiceCloneWorkflowId();
|
||||||
|
case VOICE_SYNTHESIS -> workflowConfigService.voiceSynthesisWorkflowId();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object transformResult(TaskType type, Map<String, Object> result) {
|
||||||
|
return type == TaskType.DOUYIN_COPY ? cozeService.parseDouyinCopyResult(result) : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitForCoze(ImageVideoAsyncTaskEntity task, String executeId, String cozeStatus) {
|
||||||
|
task.setStatus(TaskStatus.WAITING.name());
|
||||||
|
task.setCozeExecuteId(executeId);
|
||||||
|
task.setCozeStatus(normalizeStatus(cozeStatus));
|
||||||
|
task.setErrorMessage(null);
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
taskMapper.updateById(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void completeTask(ImageVideoAsyncTaskEntity task, Object result, String cozeStatus) {
|
||||||
|
task.setStatus(TaskStatus.SUCCESS.name());
|
||||||
|
task.setResultJson(writeJson(result));
|
||||||
|
task.setCozeStatus(normalizeStatus(cozeStatus));
|
||||||
|
task.setErrorMessage(null);
|
||||||
|
task.setCompletedAt(LocalDateTime.now());
|
||||||
|
task.setUpdatedAt(task.getCompletedAt());
|
||||||
|
taskMapper.updateById(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failTask(ImageVideoAsyncTaskEntity task, Exception ex) {
|
||||||
|
task.setStatus(TaskStatus.FAILED.name());
|
||||||
|
task.setErrorMessage(truncate(messageOf(ex)));
|
||||||
|
task.setCompletedAt(LocalDateTime.now());
|
||||||
|
task.setUpdatedAt(task.getCompletedAt());
|
||||||
|
taskMapper.updateById(task);
|
||||||
|
log.warn("[image-video] async task failed taskId={} type={} error={}",
|
||||||
|
task.getId(), task.getTaskType(), messageOf(ex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageVideoAsyncTaskEntity recoverFalseFailedTask(ImageVideoAsyncTaskEntity task) {
|
||||||
|
if (!TaskStatus.FAILED.name().equals(task.getStatus())) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
if (task.getCozeExecuteId() == null || task.getCozeExecuteId().isBlank()) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
String storedCozeStatus = normalizeStatus(task.getCozeStatus());
|
||||||
|
if (storedCozeStatus.isBlank() || TERMINAL_STATUSES.contains(storedCozeStatus)) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
String errorMessage = task.getErrorMessage() == null ? "" : task.getErrorMessage();
|
||||||
|
if (!errorMessage.startsWith("Coze workflow finished with status ")) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
task.setStatus(TaskStatus.WAITING.name());
|
||||||
|
task.setErrorMessage(null);
|
||||||
|
task.setCompletedAt(null);
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
taskMapper.updateById(task);
|
||||||
|
log.info("[image-video] recovered false failed async task taskId={} cozeStatus={}",
|
||||||
|
task.getId(), storedCozeStatus);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageVideoAsyncTaskVo toVo(ImageVideoAsyncTaskEntity task) {
|
||||||
|
ImageVideoAsyncTaskVo vo = new ImageVideoAsyncTaskVo();
|
||||||
|
vo.setTaskId(task.getId());
|
||||||
|
vo.setTaskType(task.getTaskType());
|
||||||
|
vo.setStatus(task.getStatus());
|
||||||
|
vo.setCozeExecuteId(task.getCozeExecuteId());
|
||||||
|
vo.setCozeStatus(task.getCozeStatus());
|
||||||
|
vo.setResult(readResult(task.getResultJson()));
|
||||||
|
vo.setErrorMessage(task.getErrorMessage());
|
||||||
|
vo.setSubmittedAt(task.getSubmittedAt());
|
||||||
|
vo.setCompletedAt(task.getCompletedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T readJson(String json, Class<T> type) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, type);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("Image video task payload is invalid", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object readResult(String json) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, new TypeReference<>() {});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeJson(Object value) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(value);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("Image video task serialization failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String findText(Object value, Set<String> names) {
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||||
|
String key = String.valueOf(entry.getKey());
|
||||||
|
Object child = entry.getValue();
|
||||||
|
if (names.stream().anyMatch(name -> name.equalsIgnoreCase(key)) && child != null) {
|
||||||
|
String text = String.valueOf(child).trim();
|
||||||
|
if (!text.isBlank()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Object child : map.values()) {
|
||||||
|
String nested = findText(child, names);
|
||||||
|
if (!nested.isBlank()) {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value instanceof Collection<?> collection) {
|
||||||
|
for (Object child : collection) {
|
||||||
|
String nested = findText(child, names);
|
||||||
|
if (!nested.isBlank()) {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (value instanceof String text) {
|
||||||
|
String trimmed = text.trim();
|
||||||
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
return findText(objectMapper.readValue(trimmed, Object.class), names);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveCozeExecutionStatus(Object value) {
|
||||||
|
Object data = childValue(value, "data");
|
||||||
|
String dataPrimaryStatus = findDirectText(parsePossiblyJson(data), COZE_PRIMARY_STATUS_FIELDS);
|
||||||
|
if (!dataPrimaryStatus.isBlank()) {
|
||||||
|
return normalizeStatus(dataPrimaryStatus);
|
||||||
|
}
|
||||||
|
String directPrimaryStatus = findDirectText(value, COZE_PRIMARY_STATUS_FIELDS);
|
||||||
|
if (!directPrimaryStatus.isBlank()) {
|
||||||
|
return normalizeStatus(directPrimaryStatus);
|
||||||
|
}
|
||||||
|
String dataStatus = findDirectText(parsePossiblyJson(data), COZE_EXECUTION_STATUS_FIELDS);
|
||||||
|
if (!dataStatus.isBlank()) {
|
||||||
|
return normalizeStatus(dataStatus);
|
||||||
|
}
|
||||||
|
return normalizeStatus(findDirectText(value, COZE_EXECUTION_STATUS_FIELDS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object childValue(Object value, String name) {
|
||||||
|
if (!(value instanceof Map<?, ?> map)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||||
|
if (name.equalsIgnoreCase(String.valueOf(entry.getKey()))) {
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findDirectText(Object value, Set<String> names) {
|
||||||
|
Object parsed = parsePossiblyJson(value);
|
||||||
|
if (!(parsed instanceof Map<?, ?> map)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||||
|
String key = String.valueOf(entry.getKey());
|
||||||
|
Object child = entry.getValue();
|
||||||
|
if (names.stream().anyMatch(name -> name.equalsIgnoreCase(key)) && child != null) {
|
||||||
|
String text = String.valueOf(child).trim();
|
||||||
|
if (!text.isBlank()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object parsePossiblyJson(Object value) {
|
||||||
|
if (value instanceof String text) {
|
||||||
|
String trimmed = text.trim();
|
||||||
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(trimmed, Object.class);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeStatus(String value) {
|
||||||
|
return value == null ? "" : value.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String messageOf(Exception ex) {
|
||||||
|
String message = ex.getMessage();
|
||||||
|
return message == null || message.isBlank() ? ex.getClass().getSimpleName() : message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String value) {
|
||||||
|
return value.length() <= 1000 ? value : value.substring(0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TaskStatus {
|
||||||
|
PENDING, RUNNING, WAITING, POLLING, SUCCESS, FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TaskType {
|
||||||
|
DOUYIN_COPY,
|
||||||
|
IMAGE_VIDEO_WORKFLOW,
|
||||||
|
WORKFLOW_RESULT,
|
||||||
|
VOICE_LIST,
|
||||||
|
VOICE_DELETE,
|
||||||
|
VOICE_CLONE,
|
||||||
|
VOICE_SYNTHESIS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ public class ImageVideoCozeService {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private volatile HttpClient sharedHttpClient;
|
private volatile HttpClient sharedHttpClient;
|
||||||
|
|
||||||
public DouyinCopyVo runDouyinCopy(DouyinCopyRequest request) {
|
public Map<String, Object> submitDouyinCopy(DouyinCopyRequest request) {
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new BusinessException("请求参数不能为空");
|
throw new BusinessException("请求参数不能为空");
|
||||||
}
|
}
|
||||||
@@ -69,8 +69,8 @@ public class ImageVideoCozeService {
|
|||||||
firstNonBlank(request.getApiKey(), secret.copyApiKey()),
|
firstNonBlank(request.getApiKey(), secret.copyApiKey()),
|
||||||
firstNonBlank(request.getT8Key(), secret.t8Key()),
|
firstNonBlank(request.getT8Key(), secret.t8Key()),
|
||||||
secret.cozeToken(),
|
secret.cozeToken(),
|
||||||
workflowConfigService.douyinCopyWorkflowId());
|
workflowConfigService.douyinCopyWorkflowId(), true);
|
||||||
return parseDouyinCopyResponse(responseText);
|
return parseCozeMap("douyin copy", responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> runImageVideoWorkflow(Long userId, Map<String, Object> parameters) {
|
public Map<String, Object> runImageVideoWorkflow(Long userId, Map<String, Object> parameters) {
|
||||||
@@ -94,13 +94,18 @@ public class ImageVideoCozeService {
|
|||||||
String aiConductorKey = firstNonBlank(asText(apiKeyInfo.get("ai_conductor_key")),
|
String aiConductorKey = firstNonBlank(asText(apiKeyInfo.get("ai_conductor_key")),
|
||||||
asText(resolvedParameters.get("api_key")), secret.copyApiKey());
|
asText(resolvedParameters.get("api_key")), secret.copyApiKey());
|
||||||
String t8starKey = firstNonBlank(asText(apiKeyInfo.get("t8star_key")), secret.t8Key());
|
String t8starKey = firstNonBlank(asText(apiKeyInfo.get("t8star_key")), secret.t8Key());
|
||||||
|
String t8VideoKey = firstNonBlank(asText(apiKeyInfo.get("t8_video_key")), secret.t8VideoKey());
|
||||||
if (!hasText(aiConductorKey)) {
|
if (!hasText(aiConductorKey)) {
|
||||||
throw new BusinessException("图生视频 ai_conductor_key 未配置");
|
throw new BusinessException("图生视频 ai_conductor_key 未配置");
|
||||||
}
|
}
|
||||||
if (!hasText(t8starKey)) {
|
if (!hasText(t8starKey)) {
|
||||||
throw new BusinessException("图生视频 t8star_key 未配置");
|
throw new BusinessException("图生视频 t8star_key 未配置");
|
||||||
}
|
}
|
||||||
|
if (!hasText(t8VideoKey)) {
|
||||||
|
throw new BusinessException("图生视频 t8_video_key 未配置");
|
||||||
|
}
|
||||||
apiKeyInfo.put("t8star_key", t8starKey);
|
apiKeyInfo.put("t8star_key", t8starKey);
|
||||||
|
apiKeyInfo.put("t8_video_key", t8VideoKey);
|
||||||
apiKeyInfo.put("ai_conductor_key", aiConductorKey);
|
apiKeyInfo.put("ai_conductor_key", aiConductorKey);
|
||||||
resolvedParameters.put("api_key_info", apiKeyInfo);
|
resolvedParameters.put("api_key_info", apiKeyInfo);
|
||||||
resolvedParameters.remove("api_key");
|
resolvedParameters.remove("api_key");
|
||||||
@@ -150,8 +155,27 @@ public class ImageVideoCozeService {
|
|||||||
if (!"1".equals(audioType) && !"2".equals(audioType)) {
|
if (!"1".equals(audioType) && !"2".equals(audioType)) {
|
||||||
throw new BusinessException("不支持的音频类型: " + audioType);
|
throw new BusinessException("不支持的音频类型: " + audioType);
|
||||||
}
|
}
|
||||||
normalizedAudioInfo.put("audio_url", firstNonBlank(asText(normalizedAudioInfo.get("audio_url"))));
|
String audioMode = firstNonBlank(asText(normalizedAudioInfo.get("mode")), "1");
|
||||||
|
if (!"1".equals(audioMode) && !"2".equals(audioMode) && !"3".equals(audioMode)) {
|
||||||
|
throw new BusinessException("不支持的音频模式: " + audioMode);
|
||||||
|
}
|
||||||
|
String audioUrl = firstNonBlank(asText(normalizedAudioInfo.get("audio_url")));
|
||||||
|
String bgmUrl = firstNonBlank(asText(normalizedAudioInfo.get("bgm_url")));
|
||||||
|
String voiceName = firstNonBlank(asText(normalizedAudioInfo.get("voice_name")));
|
||||||
|
if ("1".equals(audioMode)) {
|
||||||
|
bgmUrl = "";
|
||||||
|
} else if ("2".equals(audioMode)) {
|
||||||
|
audioUrl = "";
|
||||||
|
voiceName = "";
|
||||||
|
} else {
|
||||||
|
bgmUrl = "";
|
||||||
|
voiceName = "";
|
||||||
|
}
|
||||||
|
normalizedAudioInfo.put("audio_url", audioUrl);
|
||||||
normalizedAudioInfo.put("type", Integer.valueOf(audioType));
|
normalizedAudioInfo.put("type", Integer.valueOf(audioType));
|
||||||
|
normalizedAudioInfo.put("bgm_url", bgmUrl);
|
||||||
|
normalizedAudioInfo.put("mode", Integer.valueOf(audioMode));
|
||||||
|
normalizedAudioInfo.put("voice_name", voiceName);
|
||||||
parameters.put("audio_info", normalizedAudioInfo);
|
parameters.put("audio_info", normalizedAudioInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +202,7 @@ public class ImageVideoCozeService {
|
|||||||
return text.startsWith("{") && text.endsWith("}");
|
return text.startsWith("{") && text.endsWith("}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> getImageVideoWorkflowResult(Long userId, String executeId) {
|
public Map<String, Object> getWorkflowResult(Long userId, String workflowId, String executeId) {
|
||||||
String resolvedExecuteId = firstNonBlank(executeId);
|
String resolvedExecuteId = firstNonBlank(executeId);
|
||||||
if (!hasText(resolvedExecuteId)) {
|
if (!hasText(resolvedExecuteId)) {
|
||||||
throw new BusinessException("execute_id cannot be blank");
|
throw new BusinessException("execute_id cannot be blank");
|
||||||
@@ -188,13 +212,13 @@ public class ImageVideoCozeService {
|
|||||||
if (!hasText(cozeToken)) {
|
if (!hasText(cozeToken)) {
|
||||||
throw new BusinessException("图生视频 Coze token 未配置");
|
throw new BusinessException("图生视频 Coze token 未配置");
|
||||||
}
|
}
|
||||||
String workflowId = workflowConfigService.imageVideoWorkflowId();
|
String resolvedWorkflowId = firstNonBlank(workflowId);
|
||||||
if (!hasText(workflowId)) {
|
if (!hasText(resolvedWorkflowId)) {
|
||||||
throw new BusinessException("image video workflow_id 未配置");
|
throw new BusinessException("image video workflow_id 未配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
String path = firstNonBlank(properties.getCozeWorkflowHistoryPath(), "/v1/workflows/{workflow_id}/run_histories/{execute_id}")
|
String path = firstNonBlank(properties.getCozeWorkflowHistoryPath(), "/v1/workflows/{workflow_id}/run_histories/{execute_id}")
|
||||||
.replace("{workflow_id}", workflowId)
|
.replace("{workflow_id}", resolvedWorkflowId)
|
||||||
.replace("{execute_id}", resolvedExecuteId);
|
.replace("{execute_id}", resolvedExecuteId);
|
||||||
String endpoint = joinUrl(properties.getCozeBaseUrl(), path);
|
String endpoint = joinUrl(properties.getCozeBaseUrl(), path);
|
||||||
String responseText = getCozeJson("image video history", endpoint, cozeToken);
|
String responseText = getCozeJson("image video history", endpoint, cozeToken);
|
||||||
@@ -241,7 +265,7 @@ public class ImageVideoCozeService {
|
|||||||
parameters.put("api_key", secret.voiceApiKey());
|
parameters.put("api_key", secret.voiceApiKey());
|
||||||
parameters.put("name", firstNonBlank(name));
|
parameters.put("name", firstNonBlank(name));
|
||||||
parameters.put("user_id", userId);
|
parameters.put("user_id", userId);
|
||||||
return postWorkflowParameters("voice list", workflowConfigService.voiceListWorkflowId(), parameters, secret.cozeToken(), false);
|
return postWorkflowParameters("voice list", workflowConfigService.voiceListWorkflowId(), parameters, secret.cozeToken(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> runVoiceDelete(Long userId, String voiceId) {
|
public Map<String, Object> runVoiceDelete(Long userId, String voiceId) {
|
||||||
@@ -251,7 +275,7 @@ public class ImageVideoCozeService {
|
|||||||
parameters.put("group_id", secret.voiceGroupId());
|
parameters.put("group_id", secret.voiceGroupId());
|
||||||
parameters.put("user_id", userId);
|
parameters.put("user_id", userId);
|
||||||
parameters.put("voice_id", firstNonBlank(voiceId));
|
parameters.put("voice_id", firstNonBlank(voiceId));
|
||||||
return postWorkflowParameters("voice delete", workflowConfigService.voiceDeleteWorkflowId(), parameters, secret.cozeToken(), false);
|
return postWorkflowParameters("voice delete", workflowConfigService.voiceDeleteWorkflowId(), parameters, secret.cozeToken(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> runVoiceClone(Long userId, String name, String audioUrl, String videoUrl) {
|
public Map<String, Object> runVoiceClone(Long userId, String name, String audioUrl, String videoUrl) {
|
||||||
@@ -267,7 +291,7 @@ public class ImageVideoCozeService {
|
|||||||
parameters.put("user_id", userId);
|
parameters.put("user_id", userId);
|
||||||
parameters.put("audio_url", resolvedAudioUrl);
|
parameters.put("audio_url", resolvedAudioUrl);
|
||||||
parameters.put("video_url", resolvedVideoUrl);
|
parameters.put("video_url", resolvedVideoUrl);
|
||||||
return postWorkflowParameters("voice clone", workflowConfigService.voiceCloneWorkflowId(), parameters, secret.cozeToken(), false);
|
return postWorkflowParameters("voice clone", workflowConfigService.voiceCloneWorkflowId(), parameters, secret.cozeToken(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> runVoiceSynthesis(Long userId, String text, String voiceId) {
|
public Map<String, Object> runVoiceSynthesis(Long userId, String text, String voiceId) {
|
||||||
@@ -277,10 +301,11 @@ public class ImageVideoCozeService {
|
|||||||
parameters.put("group_id", secret.voiceGroupId());
|
parameters.put("group_id", secret.voiceGroupId());
|
||||||
parameters.put("text", firstNonBlank(text));
|
parameters.put("text", firstNonBlank(text));
|
||||||
parameters.put("voice_id", firstNonBlank(voiceId));
|
parameters.put("voice_id", firstNonBlank(voiceId));
|
||||||
return postWorkflowParameters("voice synthesis", workflowConfigService.voiceSynthesisWorkflowId(), parameters, secret.cozeToken(), false);
|
return postWorkflowParameters("voice synthesis", workflowConfigService.voiceSynthesisWorkflowId(), parameters, secret.cozeToken(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String postWorkflow(DouyinCopyRequest request, String url, String apiKey, String t8Key, String token, String workflowId) {
|
private String postWorkflow(DouyinCopyRequest request, String url, String apiKey, String t8Key, String token,
|
||||||
|
String workflowId, boolean async) {
|
||||||
String resolvedApiKey = firstNonBlank(apiKey);
|
String resolvedApiKey = firstNonBlank(apiKey);
|
||||||
String resolvedT8Key = firstNonBlank(t8Key);
|
String resolvedT8Key = firstNonBlank(t8Key);
|
||||||
String cozeToken = firstNonBlank(token);
|
String cozeToken = firstNonBlank(token);
|
||||||
@@ -305,6 +330,9 @@ public class ImageVideoCozeService {
|
|||||||
String resolvedWorkflowId = firstNonBlank(workflowId, properties.getDouyinCopyWorkflowId());
|
String resolvedWorkflowId = firstNonBlank(workflowId, properties.getDouyinCopyWorkflowId());
|
||||||
body.put("workflow_id", resolvedWorkflowId);
|
body.put("workflow_id", resolvedWorkflowId);
|
||||||
body.put("parameters", parameters);
|
body.put("parameters", parameters);
|
||||||
|
if (async) {
|
||||||
|
body.put("is_async", true);
|
||||||
|
}
|
||||||
|
|
||||||
String endpoint = joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath());
|
String endpoint = joinUrl(properties.getCozeBaseUrl(), properties.getCozeWorkflowPath());
|
||||||
log.info("[image-video] douyin copy coze request url={} workflowId={} inputLength={}",
|
log.info("[image-video] douyin copy coze request url={} workflowId={} inputLength={}",
|
||||||
@@ -420,6 +448,16 @@ public class ImageVideoCozeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DouyinCopyVo parseDouyinCopyResult(Map<String, Object> result) {
|
||||||
|
try {
|
||||||
|
return parseDouyinCopyResponse(objectMapper.writeValueAsString(result == null ? Map.of() : result));
|
||||||
|
} catch (BusinessException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("Coze response parse failed: " + ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String cleanCopyText(String value) {
|
private String cleanCopyText(String value) {
|
||||||
String text = firstNonBlank(value);
|
String text = firstNonBlank(value);
|
||||||
if (!hasText(text)) {
|
if (!hasText(text)) {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import java.util.Set;
|
|||||||
public class ImageVideoSecretService {
|
public class ImageVideoSecretService {
|
||||||
|
|
||||||
private static final Set<Integer> ALLOWED_EXPIRE_DAYS = Set.of(1, 7, 30);
|
private static final Set<Integer> ALLOWED_EXPIRE_DAYS = Set.of(1, 7, 30);
|
||||||
|
private static final int PERMANENT_EXPIRE_DAYS = 0;
|
||||||
|
private static final LocalDateTime PERMANENT_EXPIRES_AT = LocalDateTime.of(9999, 12, 31, 23, 59, 59);
|
||||||
|
|
||||||
private final ImageVideoSecretMapper secretMapper;
|
private final ImageVideoSecretMapper secretMapper;
|
||||||
private final ShopCredentialCryptoService cryptoService;
|
private final ShopCredentialCryptoService cryptoService;
|
||||||
@@ -34,8 +36,8 @@ public class ImageVideoSecretService {
|
|||||||
public ImageVideoSecretStatusVo save(ImageVideoSecretSaveRequest request) {
|
public ImageVideoSecretStatusVo save(ImageVideoSecretSaveRequest request) {
|
||||||
validateUserId(request.getUserId());
|
validateUserId(request.getUserId());
|
||||||
Integer expireDays = request.getExpireDays();
|
Integer expireDays = request.getExpireDays();
|
||||||
if (expireDays == null || !ALLOWED_EXPIRE_DAYS.contains(expireDays)) {
|
if (expireDays == null || (expireDays != PERMANENT_EXPIRE_DAYS && !ALLOWED_EXPIRE_DAYS.contains(expireDays))) {
|
||||||
throw new BusinessException("有效期只支持 1 天、7 天、30 天");
|
throw new BusinessException("有效期只支持 1 天、7 天、30 天、永久");
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -44,13 +46,15 @@ public class ImageVideoSecretService {
|
|||||||
|
|
||||||
String copyApiKey = normalize(request.getCopyApiKey());
|
String copyApiKey = normalize(request.getCopyApiKey());
|
||||||
String t8Key = normalize(request.getT8Key());
|
String t8Key = normalize(request.getT8Key());
|
||||||
|
String t8VideoKey = normalize(request.getT8VideoKey());
|
||||||
String voiceApiKey = normalize(request.getVoiceApiKey());
|
String voiceApiKey = normalize(request.getVoiceApiKey());
|
||||||
String voiceGroupId = normalize(request.getVoiceGroupId());
|
String voiceGroupId = normalize(request.getVoiceGroupId());
|
||||||
if (mustInputAll) {
|
if (mustInputAll) {
|
||||||
requireText(copyApiKey, "文案 key 不能为空");
|
requireText(copyApiKey, "爬虫密钥不能为空");
|
||||||
requireText(t8Key, "t8_key 不能为空");
|
requireText(t8Key, "t8_key 不能为空");
|
||||||
requireText(voiceApiKey, "音色 key 不能为空");
|
requireText(t8VideoKey, "t8_video_key 不能为空");
|
||||||
requireText(voiceGroupId, "音色 groupId 不能为空");
|
requireText(voiceApiKey, "音频密钥不能为空");
|
||||||
|
requireText(voiceGroupId, "音频团队ID不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageVideoSecretEntity row = existing == null ? new ImageVideoSecretEntity() : existing;
|
ImageVideoSecretEntity row = existing == null ? new ImageVideoSecretEntity() : existing;
|
||||||
@@ -61,13 +65,16 @@ public class ImageVideoSecretService {
|
|||||||
if (hasText(t8Key)) {
|
if (hasText(t8Key)) {
|
||||||
row.setT8Key(cryptoService.encrypt(t8Key));
|
row.setT8Key(cryptoService.encrypt(t8Key));
|
||||||
}
|
}
|
||||||
|
if (hasText(t8VideoKey)) {
|
||||||
|
row.setT8VideoKey(cryptoService.encrypt(t8VideoKey));
|
||||||
|
}
|
||||||
if (hasText(voiceApiKey)) {
|
if (hasText(voiceApiKey)) {
|
||||||
row.setVoiceApiKey(cryptoService.encrypt(voiceApiKey));
|
row.setVoiceApiKey(cryptoService.encrypt(voiceApiKey));
|
||||||
}
|
}
|
||||||
if (hasText(voiceGroupId)) {
|
if (hasText(voiceGroupId)) {
|
||||||
row.setVoiceGroupId(cryptoService.encrypt(voiceGroupId));
|
row.setVoiceGroupId(cryptoService.encrypt(voiceGroupId));
|
||||||
}
|
}
|
||||||
row.setExpiresAt(now.plusDays(expireDays));
|
row.setExpiresAt(expireDays == PERMANENT_EXPIRE_DAYS ? PERMANENT_EXPIRES_AT : now.plusDays(expireDays));
|
||||||
row.setUpdatedAt(now);
|
row.setUpdatedAt(now);
|
||||||
|
|
||||||
if (row.getId() == null) {
|
if (row.getId() == null) {
|
||||||
@@ -92,15 +99,16 @@ public class ImageVideoSecretService {
|
|||||||
String cozeToken = normalize(properties.getCozeToken());
|
String cozeToken = normalize(properties.getCozeToken());
|
||||||
String copyApiKey = decrypt(row.getCopyApiKey());
|
String copyApiKey = decrypt(row.getCopyApiKey());
|
||||||
String t8Key = decrypt(row.getT8Key());
|
String t8Key = decrypt(row.getT8Key());
|
||||||
|
String t8VideoKey = decrypt(row.getT8VideoKey());
|
||||||
String voiceApiKey = decrypt(row.getVoiceApiKey());
|
String voiceApiKey = decrypt(row.getVoiceApiKey());
|
||||||
String voiceGroupId = decrypt(row.getVoiceGroupId());
|
String voiceGroupId = decrypt(row.getVoiceGroupId());
|
||||||
if (!hasText(cozeToken)) {
|
if (!hasText(cozeToken)) {
|
||||||
throw new BusinessException("图生视频 Coze 访问令牌未配置");
|
throw new BusinessException("图生视频 Coze 访问令牌未配置");
|
||||||
}
|
}
|
||||||
if (!hasText(copyApiKey) || !hasText(t8Key) || !hasText(voiceApiKey) || !hasText(voiceGroupId)) {
|
if (!hasText(copyApiKey) || !hasText(t8Key) || !hasText(t8VideoKey) || !hasText(voiceApiKey) || !hasText(voiceGroupId)) {
|
||||||
throw new BusinessException("视频密钥未配置完整,请重新配置");
|
throw new BusinessException("视频密钥未配置完整,请重新配置");
|
||||||
}
|
}
|
||||||
return new ResolvedSecret(cozeToken, copyApiKey, t8Key, voiceApiKey, voiceGroupId);
|
return new ResolvedSecret(cozeToken, copyApiKey, t8Key, t8VideoKey, voiceApiKey, voiceGroupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResolvedSecret resolveForDouyinCopy(Long userId) {
|
public ResolvedSecret resolveForDouyinCopy(Long userId) {
|
||||||
@@ -109,17 +117,18 @@ public class ImageVideoSecretService {
|
|||||||
throw new BusinessException("图生视频 Coze 访问令牌未配置");
|
throw new BusinessException("图生视频 Coze 访问令牌未配置");
|
||||||
}
|
}
|
||||||
if (userId == null || userId <= 0) {
|
if (userId == null || userId <= 0) {
|
||||||
return new ResolvedSecret(cozeToken, "", "", "", "");
|
return new ResolvedSecret(cozeToken, "", "", "", "", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageVideoSecretEntity row = selectByUserId(userId);
|
ImageVideoSecretEntity row = selectByUserId(userId);
|
||||||
if (row == null || row.getExpiresAt() == null || !row.getExpiresAt().isAfter(LocalDateTime.now())) {
|
if (row == null || row.getExpiresAt() == null || !row.getExpiresAt().isAfter(LocalDateTime.now())) {
|
||||||
return new ResolvedSecret(cozeToken, "", "", "", "");
|
return new ResolvedSecret(cozeToken, "", "", "", "", "");
|
||||||
}
|
}
|
||||||
return new ResolvedSecret(
|
return new ResolvedSecret(
|
||||||
cozeToken,
|
cozeToken,
|
||||||
decrypt(row.getCopyApiKey()),
|
decrypt(row.getCopyApiKey()),
|
||||||
decrypt(row.getT8Key()),
|
decrypt(row.getT8Key()),
|
||||||
|
decrypt(row.getT8VideoKey()),
|
||||||
decrypt(row.getVoiceApiKey()),
|
decrypt(row.getVoiceApiKey()),
|
||||||
decrypt(row.getVoiceGroupId())
|
decrypt(row.getVoiceGroupId())
|
||||||
);
|
);
|
||||||
@@ -140,6 +149,7 @@ public class ImageVideoSecretService {
|
|||||||
vo.setExpired(false);
|
vo.setExpired(false);
|
||||||
vo.setHasCopyApiKey(false);
|
vo.setHasCopyApiKey(false);
|
||||||
vo.setHasT8Key(false);
|
vo.setHasT8Key(false);
|
||||||
|
vo.setHasT8VideoKey(false);
|
||||||
vo.setHasVoiceApiKey(false);
|
vo.setHasVoiceApiKey(false);
|
||||||
vo.setHasVoiceGroupId(false);
|
vo.setHasVoiceGroupId(false);
|
||||||
return vo;
|
return vo;
|
||||||
@@ -147,24 +157,28 @@ public class ImageVideoSecretService {
|
|||||||
|
|
||||||
String copyApiKey = decrypt(row.getCopyApiKey());
|
String copyApiKey = decrypt(row.getCopyApiKey());
|
||||||
String t8Key = decrypt(row.getT8Key());
|
String t8Key = decrypt(row.getT8Key());
|
||||||
|
String t8VideoKey = decrypt(row.getT8VideoKey());
|
||||||
String voiceApiKey = decrypt(row.getVoiceApiKey());
|
String voiceApiKey = decrypt(row.getVoiceApiKey());
|
||||||
String voiceGroupId = decrypt(row.getVoiceGroupId());
|
String voiceGroupId = decrypt(row.getVoiceGroupId());
|
||||||
boolean hasCopy = hasText(copyApiKey);
|
boolean hasCopy = hasText(copyApiKey);
|
||||||
boolean hasT8 = hasText(t8Key);
|
boolean hasT8 = hasText(t8Key);
|
||||||
|
boolean hasT8Video = hasText(t8VideoKey);
|
||||||
boolean hasVoice = hasText(voiceApiKey);
|
boolean hasVoice = hasText(voiceApiKey);
|
||||||
boolean hasGroup = hasText(voiceGroupId);
|
boolean hasGroup = hasText(voiceGroupId);
|
||||||
boolean expired = row.getExpiresAt() != null && !row.getExpiresAt().isAfter(LocalDateTime.now());
|
boolean expired = row.getExpiresAt() != null && !row.getExpiresAt().isAfter(LocalDateTime.now());
|
||||||
boolean configured = hasCopy && hasT8 && hasVoice && hasGroup;
|
boolean configured = hasCopy && hasT8 && hasT8Video && hasVoice && hasGroup;
|
||||||
|
|
||||||
vo.setConfigured(configured);
|
vo.setConfigured(configured);
|
||||||
vo.setValid(configured && !expired && row.getExpiresAt() != null);
|
vo.setValid(configured && !expired && row.getExpiresAt() != null);
|
||||||
vo.setExpired(expired);
|
vo.setExpired(expired);
|
||||||
vo.setHasCopyApiKey(hasCopy);
|
vo.setHasCopyApiKey(hasCopy);
|
||||||
vo.setHasT8Key(hasT8);
|
vo.setHasT8Key(hasT8);
|
||||||
|
vo.setHasT8VideoKey(hasT8Video);
|
||||||
vo.setHasVoiceApiKey(hasVoice);
|
vo.setHasVoiceApiKey(hasVoice);
|
||||||
vo.setHasVoiceGroupId(hasGroup);
|
vo.setHasVoiceGroupId(hasGroup);
|
||||||
vo.setCopyApiKeyMasked(mask(copyApiKey));
|
vo.setCopyApiKeyMasked(mask(copyApiKey));
|
||||||
vo.setT8KeyMasked(mask(t8Key));
|
vo.setT8KeyMasked(mask(t8Key));
|
||||||
|
vo.setT8VideoKeyMasked(mask(t8VideoKey));
|
||||||
vo.setVoiceApiKeyMasked(mask(voiceApiKey));
|
vo.setVoiceApiKeyMasked(mask(voiceApiKey));
|
||||||
vo.setVoiceGroupIdMasked(mask(voiceGroupId));
|
vo.setVoiceGroupIdMasked(mask(voiceGroupId));
|
||||||
vo.setExpireDays(resolveExpireDays(row));
|
vo.setExpireDays(resolveExpireDays(row));
|
||||||
@@ -176,6 +190,9 @@ public class ImageVideoSecretService {
|
|||||||
if (row.getExpiresAt() == null) {
|
if (row.getExpiresAt() == null) {
|
||||||
return 7;
|
return 7;
|
||||||
}
|
}
|
||||||
|
if (!row.getExpiresAt().isBefore(PERMANENT_EXPIRES_AT)) {
|
||||||
|
return PERMANENT_EXPIRE_DAYS;
|
||||||
|
}
|
||||||
LocalDateTime baseTime = row.getUpdatedAt() != null ? row.getUpdatedAt() : row.getCreatedAt();
|
LocalDateTime baseTime = row.getUpdatedAt() != null ? row.getUpdatedAt() : row.getCreatedAt();
|
||||||
if (baseTime == null) {
|
if (baseTime == null) {
|
||||||
return 7;
|
return 7;
|
||||||
@@ -228,6 +245,6 @@ public class ImageVideoSecretService {
|
|||||||
return value != null && !value.trim().isEmpty();
|
return value != null && !value.trim().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ResolvedSecret(String cozeToken, String copyApiKey, String t8Key, String voiceApiKey, String voiceGroupId) {
|
public record ResolvedSecret(String cozeToken, String copyApiKey, String t8Key, String t8VideoKey, String voiceApiKey, String voiceGroupId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,19 +111,28 @@ public class PriceTrackController {
|
|||||||
@GetMapping("/tasks/{taskId}/skip-asins/paginated")
|
@GetMapping("/tasks/{taskId}/skip-asins/paginated")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "分页查询任务的跳过 ASIN 数据",
|
summary = "分页查询任务的跳过 ASIN 数据",
|
||||||
description = "根据任务 ID 分页拉取该任务关联的 ASIN 数据。"
|
description = "根据任务编号分页拉取该任务关联的 ASIN 数据。"
|
||||||
+ "全量模式:从 biz_skip_price_asin 表查询;"
|
+ "全量模式:按任务店铺和国家从跳过跟价库查询;"
|
||||||
+ "文件模式:从任务 requestJson 中存储的上传文件数据查询。"
|
+ "文件模式:从任务保存的上传文件解析数据查询。"
|
||||||
+ "国家代码从任务的 requestJson.countryCodes 中获取。"
|
+ "未传国家过滤条件时,默认使用任务创建时选择的国家。"
|
||||||
)
|
)
|
||||||
public ApiResponse<SkipPriceAsinPageVo> getTaskSkipAsinsPaginated(
|
public ApiResponse<SkipPriceAsinPageVo> getTaskSkipAsinsPaginated(
|
||||||
@Parameter(description = "任务 ID", required = true, example = "200")
|
@Parameter(description = "任务编号", required = true, example = "200")
|
||||||
@PathVariable Long taskId,
|
@PathVariable Long taskId,
|
||||||
@Parameter(description = "页码,从 1 开始", example = "1")
|
@Parameter(description = "页码,从 1 开始", example = "1")
|
||||||
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
@RequestParam(value = "page", defaultValue = "1") Integer page,
|
||||||
@Parameter(description = "每页条数,默认 1000,最大 2000", example = "1000")
|
@Parameter(description = "每页条数,默认 1000,最大 2000", example = "1000")
|
||||||
@RequestParam(value = "page_size", defaultValue = "1000") Integer pageSize) {
|
@RequestParam(value = "page_size", defaultValue = "1000") Integer pageSize,
|
||||||
return ApiResponse.success(priceTrackTaskService.getTaskSkipAsinsPaginated(taskId, page, pageSize));
|
@Parameter(description = "店铺名称过滤条件,可重复传入,也可用英文逗号分隔;仅在任务关联店铺范围内生效", example = "测试店铺A")
|
||||||
|
@RequestParam(value = "shop_name", required = false) List<String> shopNames,
|
||||||
|
@Parameter(description = "国家代码过滤条件,可重复传入,也可用英文逗号分隔;仅在任务国家范围内生效", example = "DE")
|
||||||
|
@RequestParam(value = "country_code", required = false) List<String> countryCodes) {
|
||||||
|
return ApiResponse.success(priceTrackTaskService.getTaskSkipAsinsPaginated(
|
||||||
|
taskId,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
shopNames,
|
||||||
|
countryCodes));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/tasks/{taskId}/skip-asin/check")
|
@GetMapping("/tasks/{taskId}/skip-asin/check")
|
||||||
@@ -251,7 +260,7 @@ public class PriceTrackController {
|
|||||||
+ "未完成/处理中:本次只传增量行数据,shop.success 不传或传 null,后端仅合并缓存继续等待。"
|
+ "未完成/处理中:本次只传增量行数据,shop.success 不传或传 null,后端仅合并缓存继续等待。"
|
||||||
+ "已完成:传最后一批数据并设置 shop.success=true,后端会使用该店铺累计后的完整数据出结果。"
|
+ "已完成:传最后一批数据并设置 shop.success=true,后端会使用该店铺累计后的完整数据出结果。"
|
||||||
+ "失败:传 shop.success=false 或传非空 shop.error。"
|
+ "失败:传 shop.success=false 或传非空 shop.error。"
|
||||||
+ "行级数据只保留表头字段:shopMallName、asin、price、recommendedPrice、shippingFee、minimumPrice、firstPlace、secondPlace、cartShopName、priceChangeStatus、modifyCount、status。"
|
+ "行级数据只保留表头字段:shopMallName、asin、price、recommendedPrice、shippingFee、minimumPrice、firstPlace、firstShop、secondPlace、secondShop、cartShopName、priceChangeStatus、modifyCount、status。"
|
||||||
)
|
)
|
||||||
public ApiResponse<Void> submitResult(
|
public ApiResponse<Void> submitResult(
|
||||||
@Parameter(
|
@Parameter(
|
||||||
|
|||||||
@@ -83,9 +83,17 @@ public class PriceTrackSubmitResultRequest {
|
|||||||
@Schema(description = "第一名", example = "竞对A")
|
@Schema(description = "第一名", example = "竞对A")
|
||||||
private String firstPlace;
|
private String firstPlace;
|
||||||
|
|
||||||
|
@JsonAlias({"first_shop", "firstPlaceShop", "first_place_shop", "第一名店铺名"})
|
||||||
|
@Schema(description = "第一名店铺名", example = "店铺A")
|
||||||
|
private String firstShop;
|
||||||
|
|
||||||
@Schema(description = "第二名", example = "竞对B")
|
@Schema(description = "第二名", example = "竞对B")
|
||||||
private String secondPlace;
|
private String secondPlace;
|
||||||
|
|
||||||
|
@JsonAlias({"second_shop", "secondPlaceShop", "second_place_shop", "第二名店铺名"})
|
||||||
|
@Schema(description = "第二名店铺名", example = "店铺B")
|
||||||
|
private String secondShop;
|
||||||
|
|
||||||
@Schema(description = "购物车店铺名", example = "店铺A")
|
@Schema(description = "购物车店铺名", example = "店铺A")
|
||||||
private String cartShopName;
|
private String cartShopName;
|
||||||
|
|
||||||
|
|||||||
@@ -9,47 +9,47 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Price-track skipped ASIN page response.
|
* 跟价任务跳过 ASIN 分页响应。
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "Price-track skipped ASIN page response")
|
@Schema(description = "跟价任务跳过 ASIN 分页响应")
|
||||||
public class SkipPriceAsinPageVo {
|
public class SkipPriceAsinPageVo {
|
||||||
|
|
||||||
@Schema(description = "Current page, starting from 1")
|
@Schema(description = "当前页码,从 1 开始")
|
||||||
private Integer page;
|
private Integer page;
|
||||||
|
|
||||||
@Schema(description = "Page size")
|
@Schema(description = "每页条数")
|
||||||
private Integer pageSize;
|
private Integer pageSize;
|
||||||
|
|
||||||
@Schema(description = "Total records")
|
@Schema(description = "总记录数")
|
||||||
private Long total;
|
private Long total;
|
||||||
|
|
||||||
@Schema(description = "Total pages")
|
@Schema(description = "总页数")
|
||||||
private Integer totalPages;
|
private Integer totalPages;
|
||||||
|
|
||||||
@Schema(description = "ASIN list grouped by country")
|
@Schema(description = "按国家分组的 ASIN 列表")
|
||||||
private Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
private Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||||
|
|
||||||
@Schema(description = "ASIN detail list grouped by country, including asin and minimumPrice")
|
@Schema(description = "按国家分组的 ASIN 明细列表,包含 ASIN 和最低价")
|
||||||
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
|
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountry = new LinkedHashMap<>();
|
||||||
|
|
||||||
@JsonProperty("skip_asins")
|
@JsonProperty("skip_asins")
|
||||||
@Schema(description = "Legacy Python queue field: ASIN list grouped by country")
|
@Schema(description = "兼容旧版队列字段:按国家分组的 ASIN 列表")
|
||||||
private Map<String, List<String>> skipAsins = new LinkedHashMap<>();
|
private Map<String, List<String>> skipAsins = new LinkedHashMap<>();
|
||||||
|
|
||||||
@JsonProperty("skip_asins_by_country")
|
@JsonProperty("skip_asins_by_country")
|
||||||
@Schema(description = "Legacy Python queue field: ASIN list grouped by country")
|
@Schema(description = "兼容旧版队列字段:按国家分组的 ASIN 列表")
|
||||||
private Map<String, List<String>> skipAsinsByCountryLegacy = new LinkedHashMap<>();
|
private Map<String, List<String>> skipAsinsByCountryLegacy = new LinkedHashMap<>();
|
||||||
|
|
||||||
@JsonProperty("skip_asin_details_by_country")
|
@JsonProperty("skip_asin_details_by_country")
|
||||||
@Schema(description = "Legacy Python queue field: ASIN detail list grouped by country")
|
@Schema(description = "兼容旧版队列字段:按国家分组的 ASIN 明细列表")
|
||||||
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountryLegacy = new LinkedHashMap<>();
|
private Map<String, List<Map<String, String>>> skipAsinDetailsByCountryLegacy = new LinkedHashMap<>();
|
||||||
|
|
||||||
@JsonProperty("asin_rows_by_country")
|
@JsonProperty("asin_rows_by_country")
|
||||||
@Schema(description = "Legacy Python queue field: appointed ASIN rows grouped by country")
|
@Schema(description = "兼容旧版队列字段:按国家分组的指定 ASIN 文件行数据")
|
||||||
private Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
|
private Map<String, List<Map<String, String>>> asinRowsByCountry = new LinkedHashMap<>();
|
||||||
|
|
||||||
@JsonProperty("minimum_price_by_country_and_asin")
|
@JsonProperty("minimum_price_by_country_and_asin")
|
||||||
@Schema(description = "Legacy Python queue field: minimum price map grouped by country and ASIN")
|
@Schema(description = "兼容旧版队列字段:按国家和 ASIN 分组的最低价映射")
|
||||||
private Map<String, Map<String, String>> minimumPriceByCountryAndAsin = new LinkedHashMap<>();
|
private Map<String, Map<String, String>> minimumPriceByCountryAndAsin = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
"运费",
|
"运费",
|
||||||
"最低价",
|
"最低价",
|
||||||
"第一名",
|
"第一名",
|
||||||
|
"第一名店铺名",
|
||||||
"第二名",
|
"第二名",
|
||||||
|
"第二名店铺名",
|
||||||
"购物车店铺名",
|
"购物车店铺名",
|
||||||
"改价价格",
|
"改价价格",
|
||||||
"改价情况",
|
"改价情况",
|
||||||
@@ -62,12 +64,14 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
row.createCell(4).setCellValue(valueOf(item.getShippingFee()));
|
row.createCell(4).setCellValue(valueOf(item.getShippingFee()));
|
||||||
row.createCell(5).setCellValue(valueOf(item.getMinimumPrice()));
|
row.createCell(5).setCellValue(valueOf(item.getMinimumPrice()));
|
||||||
row.createCell(6).setCellValue(valueOf(item.getFirstPlace()));
|
row.createCell(6).setCellValue(valueOf(item.getFirstPlace()));
|
||||||
row.createCell(7).setCellValue(valueOf(item.getSecondPlace()));
|
row.createCell(7).setCellValue(valueOf(item.getFirstShop()));
|
||||||
row.createCell(8).setCellValue(valueOf(item.getCartShopName()));
|
row.createCell(8).setCellValue(valueOf(item.getSecondPlace()));
|
||||||
row.createCell(9).setCellValue(valueOf(item.getPriceChangePrice()));
|
row.createCell(9).setCellValue(valueOf(item.getSecondShop()));
|
||||||
row.createCell(10).setCellValue(valueOf(item.getPriceChangeStatus()));
|
row.createCell(10).setCellValue(valueOf(item.getCartShopName()));
|
||||||
row.createCell(11).setCellValue(valueOf(item.getModifyCount()));
|
row.createCell(11).setCellValue(valueOf(item.getPriceChangePrice()));
|
||||||
row.createCell(12).setCellValue(valueOf(item.getStatus()));
|
row.createCell(12).setCellValue(valueOf(item.getPriceChangeStatus()));
|
||||||
|
row.createCell(13).setCellValue(valueOf(item.getModifyCount()));
|
||||||
|
row.createCell(14).setCellValue(valueOf(item.getStatus()));
|
||||||
}
|
}
|
||||||
applyDefaultColumnWidths(sheet);
|
applyDefaultColumnWidths(sheet);
|
||||||
}
|
}
|
||||||
@@ -127,7 +131,7 @@ public class PriceTrackExcelAssemblyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyDefaultColumnWidths(Sheet sheet) {
|
private void applyDefaultColumnWidths(Sheet sheet) {
|
||||||
int[] widths = {22, 18, 12, 12, 12, 12, 20, 20, 22, 14, 18, 12, 12};
|
int[] widths = {22, 18, 12, 12, 12, 12, 20, 22, 20, 22, 22, 14, 18, 12, 12};
|
||||||
for (int i = 0; i < widths.length; i++) {
|
for (int i = 0; i < widths.length; i++) {
|
||||||
sheet.setColumnWidth(i, widths[i] * 256);
|
sheet.setColumnWidth(i, widths[i] * 256);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -816,6 +817,11 @@ public class PriceTrackTaskService {
|
|||||||
*/
|
*/
|
||||||
public com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskSkipAsinsPaginated(
|
public com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskSkipAsinsPaginated(
|
||||||
Long taskId, int page, int pageSize) {
|
Long taskId, int page, int pageSize) {
|
||||||
|
return getTaskSkipAsinsPaginated(taskId, page, pageSize, List.of(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo getTaskSkipAsinsPaginated(
|
||||||
|
Long taskId, int page, int pageSize, List<String> requestedShopNames, List<String> requestedCountryCodes) {
|
||||||
if (taskId == null || taskId <= 0) {
|
if (taskId == null || taskId <= 0) {
|
||||||
throw new BusinessException("taskId 不合法");
|
throw new BusinessException("taskId 不合法");
|
||||||
}
|
}
|
||||||
@@ -827,8 +833,10 @@ public class PriceTrackTaskService {
|
|||||||
|
|
||||||
PriceTrackCreateTaskRequest request = parseTaskRequest(task);
|
PriceTrackCreateTaskRequest request = parseTaskRequest(task);
|
||||||
|
|
||||||
// Resolve country codes from task request.
|
List<String> countryCodes = resolveSkipAsinCountryCodes(request, requestedCountryCodes);
|
||||||
List<String> countryCodes = request.getCountryCodes();
|
if (countryCodes.isEmpty()) {
|
||||||
|
return emptySkipAsinPage(List.of(), page, pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Choose full mode or uploaded-ASIN file mode.
|
// Choose full mode or uploaded-ASIN file mode.
|
||||||
boolean isAsinMode = request.isAsinMode();
|
boolean isAsinMode = request.isAsinMode();
|
||||||
@@ -841,11 +849,116 @@ public class PriceTrackTaskService {
|
|||||||
// File mode reads parsed uploaded ASIN rows.
|
// File mode reads parsed uploaded ASIN rows.
|
||||||
return getTaskAsinFilePaginated(request, countryCodes, page, pageSize);
|
return getTaskAsinFilePaginated(request, countryCodes, page, pageSize);
|
||||||
} else {
|
} else {
|
||||||
// Full mode uses all ASINs and minimum prices from biz_skip_price_asin.
|
List<String> shopNames = resolveSkipAsinShopNames(request, requestedShopNames);
|
||||||
return skipPriceAsinService.listSkipAsinsPaginated(List.of(), countryCodes, page, pageSize);
|
if (shopNames.isEmpty()) {
|
||||||
|
return emptySkipAsinPage(countryCodes, page, pageSize);
|
||||||
|
}
|
||||||
|
return skipPriceAsinService.listSkipAsinsPaginated(shopNames, countryCodes, page, pageSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> resolveSkipAsinCountryCodes(PriceTrackCreateTaskRequest request, List<String> requestedCountryCodes) {
|
||||||
|
List<String> taskCountryCodes = normalizeTaskCountryCodes(request == null ? null : request.getCountryCodes());
|
||||||
|
LinkedHashSet<String> requested = new LinkedHashSet<>();
|
||||||
|
for (String country : splitQueryValues(requestedCountryCodes)) {
|
||||||
|
requested.add(normalizeLookupCountry(country));
|
||||||
|
}
|
||||||
|
if (requested.isEmpty()) {
|
||||||
|
return taskCountryCodes;
|
||||||
|
}
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
for (String country : requested) {
|
||||||
|
if (isTaskCountryEnabled(taskCountryCodes, country)) {
|
||||||
|
result.add(country);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> normalizeTaskCountryCodes(List<String> countryCodes) {
|
||||||
|
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||||
|
if (countryCodes != null) {
|
||||||
|
for (String country : countryCodes) {
|
||||||
|
String value = country == null ? "" : country.trim().toUpperCase(Locale.ROOT);
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
normalized.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveSkipAsinShopNames(PriceTrackCreateTaskRequest request, List<String> requestedShopNames) {
|
||||||
|
List<String> taskShopNames = resolveTaskShopNames(request);
|
||||||
|
List<String> requested = splitQueryValues(requestedShopNames);
|
||||||
|
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||||
|
for (String shopName : requested) {
|
||||||
|
String value = ziniaoShopSwitchService.normalizeShopName(shopName);
|
||||||
|
if (!value.isBlank() && (taskShopNames.isEmpty() || taskShopNames.contains(value))) {
|
||||||
|
normalized.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!normalized.isEmpty()) {
|
||||||
|
return new ArrayList<>(normalized);
|
||||||
|
}
|
||||||
|
if (!requested.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return taskShopNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> splitQueryValues(List<String> values) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (values == null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
for (String value : values) {
|
||||||
|
if (value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String part : value.split(",")) {
|
||||||
|
String trimmed = part.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
result.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo emptySkipAsinPage(
|
||||||
|
List<String> countryCodes, int page, int pageSize) {
|
||||||
|
if (page < 1) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
if (pageSize < 1 || pageSize > 2000) {
|
||||||
|
pageSize = 1000;
|
||||||
|
}
|
||||||
|
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
|
||||||
|
? List.of("DE", "UK", "FR", "IT", "ES")
|
||||||
|
: countryCodes;
|
||||||
|
Map<String, List<String>> skipAsinsByCountry = new LinkedHashMap<>();
|
||||||
|
Map<String, List<Map<String, String>>> skipDetailsByCountry = new LinkedHashMap<>();
|
||||||
|
for (String country : targetCountries) {
|
||||||
|
skipAsinsByCountry.put(country, new ArrayList<>());
|
||||||
|
skipDetailsByCountry.put(country, new ArrayList<>());
|
||||||
|
}
|
||||||
|
com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo vo =
|
||||||
|
new com.nanri.aiimage.modules.pricetrack.model.vo.SkipPriceAsinPageVo();
|
||||||
|
vo.setPage(page);
|
||||||
|
vo.setPageSize(pageSize);
|
||||||
|
vo.setTotal(0L);
|
||||||
|
vo.setTotalPages(0);
|
||||||
|
vo.setSkipAsinsByCountry(skipAsinsByCountry);
|
||||||
|
vo.setSkipAsinDetailsByCountry(skipDetailsByCountry);
|
||||||
|
vo.setSkipAsins(skipAsinsByCountry);
|
||||||
|
vo.setSkipAsinsByCountryLegacy(skipAsinsByCountry);
|
||||||
|
vo.setSkipAsinDetailsByCountryLegacy(skipDetailsByCountry);
|
||||||
|
vo.setAsinRowsByCountry(new LinkedHashMap<>());
|
||||||
|
vo.setMinimumPriceByCountryAndAsin(new LinkedHashMap<>());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> resolveTaskShopNames(PriceTrackCreateTaskRequest request) {
|
private List<String> resolveTaskShopNames(PriceTrackCreateTaskRequest request) {
|
||||||
List<String> shopNames = new ArrayList<>();
|
List<String> shopNames = new ArrayList<>();
|
||||||
if (request == null || request.getItems() == null) {
|
if (request == null || request.getItems() == null) {
|
||||||
@@ -1391,7 +1504,9 @@ public class PriceTrackTaskService {
|
|||||||
case "推荐价", "recommendedprice" -> "recommendedPrice";
|
case "推荐价", "recommendedprice" -> "recommendedPrice";
|
||||||
case "最低价", "minimumprice" -> "minimumPrice";
|
case "最低价", "minimumprice" -> "minimumPrice";
|
||||||
case "第一名", "firstplace" -> "firstPlace";
|
case "第一名", "firstplace" -> "firstPlace";
|
||||||
|
case "第一名店铺名", "firstshop", "firstplaceshop" -> "firstShop";
|
||||||
case "第二名", "secondplace" -> "secondPlace";
|
case "第二名", "secondplace" -> "secondPlace";
|
||||||
|
case "第二名店铺名", "secondshop", "secondplaceshop" -> "secondShop";
|
||||||
case "购物车店铺名", "cartshopname" -> "cartShopName";
|
case "购物车店铺名", "cartshopname" -> "cartShopName";
|
||||||
case "改价情况", "pricechangestatus" -> "priceChangeStatus";
|
case "改价情况", "pricechangestatus" -> "priceChangeStatus";
|
||||||
case "修改次数", "modifycount" -> "modifyCount";
|
case "修改次数", "modifycount" -> "modifyCount";
|
||||||
@@ -1661,7 +1776,9 @@ public class PriceTrackTaskService {
|
|||||||
merged.setShippingFee(firstNonBlank(incoming.getShippingFee(), merged.getShippingFee()));
|
merged.setShippingFee(firstNonBlank(incoming.getShippingFee(), merged.getShippingFee()));
|
||||||
merged.setMinimumPrice(firstNonBlank(incoming.getMinimumPrice(), merged.getMinimumPrice()));
|
merged.setMinimumPrice(firstNonBlank(incoming.getMinimumPrice(), merged.getMinimumPrice()));
|
||||||
merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace()));
|
merged.setFirstPlace(firstNonBlank(incoming.getFirstPlace(), merged.getFirstPlace()));
|
||||||
|
merged.setFirstShop(firstNonBlank(incoming.getFirstShop(), merged.getFirstShop()));
|
||||||
merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace()));
|
merged.setSecondPlace(firstNonBlank(incoming.getSecondPlace(), merged.getSecondPlace()));
|
||||||
|
merged.setSecondShop(firstNonBlank(incoming.getSecondShop(), merged.getSecondShop()));
|
||||||
merged.setCartShopName(firstNonBlank(incoming.getCartShopName(), merged.getCartShopName()));
|
merged.setCartShopName(firstNonBlank(incoming.getCartShopName(), merged.getCartShopName()));
|
||||||
merged.setPriceChangePrice(firstNonBlank(incoming.getPriceChangePrice(), merged.getPriceChangePrice()));
|
merged.setPriceChangePrice(firstNonBlank(incoming.getPriceChangePrice(), merged.getPriceChangePrice()));
|
||||||
merged.setPriceChangeStatus(firstNonBlank(incoming.getPriceChangeStatus(), merged.getPriceChangeStatus()));
|
merged.setPriceChangeStatus(firstNonBlank(incoming.getPriceChangeStatus(), merged.getPriceChangeStatus()));
|
||||||
@@ -1686,7 +1803,9 @@ public class PriceTrackTaskService {
|
|||||||
out.setShippingFee(row.getShippingFee());
|
out.setShippingFee(row.getShippingFee());
|
||||||
out.setMinimumPrice(row.getMinimumPrice());
|
out.setMinimumPrice(row.getMinimumPrice());
|
||||||
out.setFirstPlace(row.getFirstPlace());
|
out.setFirstPlace(row.getFirstPlace());
|
||||||
|
out.setFirstShop(row.getFirstShop());
|
||||||
out.setSecondPlace(row.getSecondPlace());
|
out.setSecondPlace(row.getSecondPlace());
|
||||||
|
out.setSecondShop(row.getSecondShop());
|
||||||
out.setCartShopName(row.getCartShopName());
|
out.setCartShopName(row.getCartShopName());
|
||||||
out.setPriceChangePrice(row.getPriceChangePrice());
|
out.setPriceChangePrice(row.getPriceChangePrice());
|
||||||
out.setPriceChangeStatus(row.getPriceChangeStatus());
|
out.setPriceChangeStatus(row.getPriceChangeStatus());
|
||||||
|
|||||||
@@ -1144,6 +1144,10 @@ public class SkipPriceAsinService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
|
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
|
||||||
|
? List.of("DE", "UK", "FR", "IT", "ES")
|
||||||
|
: countryCodes;
|
||||||
|
|
||||||
LambdaQueryWrapper<SkipPriceAsinEntity> queryWrapper = new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
LambdaQueryWrapper<SkipPriceAsinEntity> queryWrapper = new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
||||||
.orderByDesc(SkipPriceAsinEntity::getId);
|
.orderByDesc(SkipPriceAsinEntity::getId);
|
||||||
|
|
||||||
@@ -1160,14 +1164,11 @@ public class SkipPriceAsinService {
|
|||||||
if (!normalizedShopNames.isEmpty()) {
|
if (!normalizedShopNames.isEmpty()) {
|
||||||
queryWrapper.in(SkipPriceAsinEntity::getShopName, normalizedShopNames);
|
queryWrapper.in(SkipPriceAsinEntity::getShopName, normalizedShopNames);
|
||||||
}
|
}
|
||||||
|
applyCountryAsinFilter(queryWrapper, targetCountries);
|
||||||
|
|
||||||
// 查询所有记录(用于内存分页)
|
// 查询所有记录(用于内存分页)
|
||||||
List<SkipPriceAsinEntity> allRecords = skipPriceAsinMapper.selectList(queryWrapper);
|
List<SkipPriceAsinEntity> allRecords = skipPriceAsinMapper.selectList(queryWrapper);
|
||||||
|
|
||||||
List<String> targetCountries = (countryCodes == null || countryCodes.isEmpty())
|
|
||||||
? List.of("DE", "UK", "FR", "IT", "ES")
|
|
||||||
: countryCodes;
|
|
||||||
|
|
||||||
// 展开所有 ASIN 为扁平列表
|
// 展开所有 ASIN 为扁平列表
|
||||||
List<AsinItem> flattenedAsins = new ArrayList<>();
|
List<AsinItem> flattenedAsins = new ArrayList<>();
|
||||||
for (SkipPriceAsinEntity row : allRecords) {
|
for (SkipPriceAsinEntity row : allRecords) {
|
||||||
@@ -1228,6 +1229,41 @@ public class SkipPriceAsinService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyCountryAsinFilter(LambdaQueryWrapper<SkipPriceAsinEntity> queryWrapper, List<String> countries) {
|
||||||
|
LinkedHashSet<String> normalizedCountries = new LinkedHashSet<>();
|
||||||
|
if (countries != null) {
|
||||||
|
for (String country : countries) {
|
||||||
|
String normalized = normalizeBlank(country).toUpperCase(Locale.ROOT);
|
||||||
|
switch (normalized) {
|
||||||
|
case "DE", "UK", "FR", "IT", "ES" -> normalizedCountries.add(normalized);
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalizedCountries.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queryWrapper.and(wrapper -> {
|
||||||
|
boolean[] first = {true};
|
||||||
|
for (String country : normalizedCountries) {
|
||||||
|
if (!first[0]) {
|
||||||
|
wrapper.or();
|
||||||
|
}
|
||||||
|
switch (country) {
|
||||||
|
case "DE" -> wrapper.ne(SkipPriceAsinEntity::getAsinDe, "");
|
||||||
|
case "UK" -> wrapper.ne(SkipPriceAsinEntity::getAsinUk, "");
|
||||||
|
case "FR" -> wrapper.ne(SkipPriceAsinEntity::getAsinFr, "");
|
||||||
|
case "IT" -> wrapper.ne(SkipPriceAsinEntity::getAsinIt, "");
|
||||||
|
case "ES" -> wrapper.ne(SkipPriceAsinEntity::getAsinEs, "");
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first[0] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 内部辅助类
|
// 内部辅助类
|
||||||
private static class AsinItem {
|
private static class AsinItem {
|
||||||
String country;
|
String country;
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ aiimage:
|
|||||||
coze-workflow-path: ${AIIMAGE_IMAGE_VIDEO_COZE_WORKFLOW_PATH:/v1/workflow/run}
|
coze-workflow-path: ${AIIMAGE_IMAGE_VIDEO_COZE_WORKFLOW_PATH:/v1/workflow/run}
|
||||||
coze-connect-timeout-millis: ${AIIMAGE_IMAGE_VIDEO_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
coze-connect-timeout-millis: ${AIIMAGE_IMAGE_VIDEO_COZE_CONNECT_TIMEOUT_MILLIS:10000}
|
||||||
coze-read-timeout-millis: ${AIIMAGE_IMAGE_VIDEO_COZE_READ_TIMEOUT_MILLIS:3600000}
|
coze-read-timeout-millis: ${AIIMAGE_IMAGE_VIDEO_COZE_READ_TIMEOUT_MILLIS:3600000}
|
||||||
|
async-task-dispatch-delay-ms: ${AIIMAGE_IMAGE_VIDEO_ASYNC_TASK_DISPATCH_DELAY_MS:1000}
|
||||||
|
async-task-poll-delay-ms: ${AIIMAGE_IMAGE_VIDEO_ASYNC_TASK_POLL_DELAY_MS:5000}
|
||||||
security:
|
security:
|
||||||
shop-credential-key: ${AIIMAGE_SHOP_CREDENTIAL_KEY:change-me-shop-credential-key}
|
shop-credential-key: ${AIIMAGE_SHOP_CREDENTIAL_KEY:change-me-shop-credential-key}
|
||||||
internal-token: ${AIIMAGE_INTERNAL_TOKEN:}
|
internal-token: ${AIIMAGE_INTERNAL_TOKEN:}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS biz_image_video_async_task (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
task_type VARCHAR(32) NOT NULL,
|
||||||
|
status VARCHAR(16) NOT NULL,
|
||||||
|
request_json LONGTEXT NOT NULL,
|
||||||
|
result_json LONGTEXT NULL,
|
||||||
|
error_message VARCHAR(1000) NULL,
|
||||||
|
coze_execute_id VARCHAR(128) NULL,
|
||||||
|
coze_status VARCHAR(32) NULL,
|
||||||
|
attempt_count INT NOT NULL DEFAULT 0,
|
||||||
|
submitted_at DATETIME NOT NULL,
|
||||||
|
completed_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_image_video_async_user (user_id, id),
|
||||||
|
KEY idx_image_video_async_status (status, updated_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Image video Coze async tasks';
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE `biz_image_video_secret`
|
||||||
|
ADD COLUMN `t8_video_key` VARCHAR(1024) NULL COMMENT 'Encrypted domestic T8 video key' AFTER `t8_key`;
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="delivery-field">
|
<div class="delivery-field">
|
||||||
<span class="delivery-field__label">整体风格 / 视频提示词</span>
|
<span class="delivery-field__label">视频风格/动作引导</span>
|
||||||
<el-input v-model="currentWorkspace.videoPrompt" type="textarea" :rows="4" resize="none"
|
<el-input v-model="currentWorkspace.videoPrompt" type="textarea" :rows="4" resize="none"
|
||||||
placeholder="例如:视频画面保持高清,运镜平稳,整体风格干净明亮。" />
|
placeholder="例如:视频画面保持高清,运镜平稳,整体风格干净明亮。" />
|
||||||
</div>
|
</div>
|
||||||
@@ -147,16 +147,18 @@
|
|||||||
placeholder="例如:防滑耐磨、复古网面透气设计。" />
|
placeholder="例如:防滑耐磨、复古网面透气设计。" />
|
||||||
</div>
|
</div>
|
||||||
<div class="product-assets">
|
<div class="product-assets">
|
||||||
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index" :title="`产品图 ${index + 1}`"
|
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index"
|
||||||
:description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''" placeholder="上传产品图"
|
:title="`产品图 ${index + 1}`" :description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''"
|
||||||
helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName" :preview-url="asset.previewUrl"
|
placeholder="上传产品图" helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName"
|
||||||
:preview-kind="asset.previewKind" :source-url="asset.sourceUrl" :loading="asset.uploading" allow-url compact
|
:preview-url="asset.previewUrl" :preview-kind="asset.previewKind" :source-url="asset.sourceUrl"
|
||||||
preview-clickable action-label="上传产品图" selected-label="产品图" url-label="产品图链接"
|
:loading="asset.uploading" allow-url compact preview-clickable action-label="上传产品图" selected-label="产品图"
|
||||||
url-placeholder="也可以粘贴产品图链接" @select="(file) => setProductAsset(activeTab, index, file)"
|
url-label="产品图链接" url-placeholder="也可以粘贴产品图链接"
|
||||||
|
@select="(file) => setProductAsset(activeTab, index, file)"
|
||||||
@url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
|
@url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
|
||||||
@clear="clearProductAsset(activeTab, index)" />
|
@clear="clearProductAsset(activeTab, index)" />
|
||||||
<button v-if="currentWorkspace.productAssets.length < 5" type="button" class="script-action-btn product-assets__add"
|
<button v-if="currentWorkspace.productAssets.length < 5" type="button"
|
||||||
@click="addProductAsset(activeTab)">添加产品图({{ currentWorkspace.productAssets.length }}/5)</button>
|
class="script-action-btn product-assets__add" @click="addProductAsset(activeTab)">添加产品图({{
|
||||||
|
currentWorkspace.productAssets.length }}/5)</button>
|
||||||
</div>
|
</div>
|
||||||
</AiSectionCard>
|
</AiSectionCard>
|
||||||
|
|
||||||
@@ -205,16 +207,18 @@
|
|||||||
placeholder="例如:防滑耐磨、复古网面透气设计。" />
|
placeholder="例如:防滑耐磨、复古网面透气设计。" />
|
||||||
</div>
|
</div>
|
||||||
<div class="product-assets">
|
<div class="product-assets">
|
||||||
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index" :title="`产品图 ${index + 1}`"
|
<AiAssetDropzone v-for="(asset, index) in currentWorkspace.productAssets" :key="index"
|
||||||
:description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''" placeholder="上传产品图"
|
:title="`产品图 ${index + 1}`" :description="index === 0 ? '最多上传 5 张,提交时按接口数组格式传入。' : ''"
|
||||||
helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName" :preview-url="asset.previewUrl"
|
placeholder="上传产品图" helper="支持 JPG / PNG" accept="image/*" :file-name="asset.fileName"
|
||||||
:preview-kind="asset.previewKind" :source-url="asset.sourceUrl" :loading="asset.uploading" allow-url compact
|
:preview-url="asset.previewUrl" :preview-kind="asset.previewKind" :source-url="asset.sourceUrl"
|
||||||
preview-clickable action-label="上传产品图" selected-label="产品图" url-label="产品图链接"
|
:loading="asset.uploading" allow-url compact preview-clickable action-label="上传产品图" selected-label="产品图"
|
||||||
url-placeholder="也可以粘贴产品图链接" @select="(file) => setProductAsset(activeTab, index, file)"
|
url-label="产品图链接" url-placeholder="也可以粘贴产品图链接"
|
||||||
|
@select="(file) => setProductAsset(activeTab, index, file)"
|
||||||
@url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
|
@url-change="(url) => setProductAssetUrl(activeTab, index, url)" @preview="openImagePreview"
|
||||||
@clear="clearProductAsset(activeTab, index)" />
|
@clear="clearProductAsset(activeTab, index)" />
|
||||||
<button v-if="currentWorkspace.productAssets.length < 5" type="button" class="script-action-btn product-assets__add"
|
<button v-if="currentWorkspace.productAssets.length < 5" type="button"
|
||||||
@click="addProductAsset(activeTab)">添加产品图({{ currentWorkspace.productAssets.length }}/5)</button>
|
class="script-action-btn product-assets__add" @click="addProductAsset(activeTab)">添加产品图({{
|
||||||
|
currentWorkspace.productAssets.length }}/5)</button>
|
||||||
</div>
|
</div>
|
||||||
</AiSectionCard>
|
</AiSectionCard>
|
||||||
|
|
||||||
@@ -260,12 +264,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="currentWorkspace.voiceMode === 'uploadAudio'">
|
<template v-if="currentWorkspace.voiceMode === 'uploadAudio'">
|
||||||
<AiAssetDropzone title="最终配音文件" description="对应 text_info.file_url;只放最终用于视频配音的音频。" placeholder="上传音频文件"
|
<AiAssetDropzone title="背景音乐文件" description="对应 audio_info.bgm_url;仅在背景音乐模式传入。" placeholder="上传背景音乐"
|
||||||
helper="支持 MP3 / WAV,也可粘贴可访问的音频链接" accept="audio/*" :file-name="currentWorkspace.speechAsset.fileName"
|
helper="支持 MP3 / WAV,也可粘贴可访问的音频链接" accept="audio/*" :file-name="currentWorkspace.speechAsset.fileName"
|
||||||
:preview-url="currentWorkspace.speechAsset.previewUrl"
|
:preview-url="currentWorkspace.speechAsset.previewUrl"
|
||||||
:preview-kind="currentWorkspace.speechAsset.previewKind"
|
:preview-kind="currentWorkspace.speechAsset.previewKind"
|
||||||
:source-url="currentWorkspace.speechAsset.sourceUrl" :loading="currentWorkspace.speechAsset.uploading"
|
:source-url="currentWorkspace.speechAsset.sourceUrl" :loading="currentWorkspace.speechAsset.uploading"
|
||||||
allow-url compact action-label="上传音频" selected-label="最终配音" url-label="最终配音文件链接"
|
allow-url compact action-label="上传背景音乐" selected-label="背景音乐" url-label="背景音乐链接"
|
||||||
@select="(file) => setAsset(activeTab, 'speechAsset', file)"
|
@select="(file) => setAsset(activeTab, 'speechAsset', file)"
|
||||||
@url-change="(url) => setAssetUrl(activeTab, 'speechAsset', url, 'audio/*')"
|
@url-change="(url) => setAssetUrl(activeTab, 'speechAsset', url, 'audio/*')"
|
||||||
@clear="clearAsset(activeTab, 'speechAsset')" />
|
@clear="clearAsset(activeTab, 'speechAsset')" />
|
||||||
@@ -275,13 +279,16 @@
|
|||||||
<div class="delivery-form-grid delivery-form-grid--two">
|
<div class="delivery-form-grid delivery-form-grid--two">
|
||||||
<div class="delivery-field">
|
<div class="delivery-field">
|
||||||
<span class="delivery-field__label">合成音色</span>
|
<span class="delivery-field__label">合成音色</span>
|
||||||
<el-input v-model="currentWorkspace.voiceId" clearable placeholder="选择音色或手动输入 voice_id"
|
<el-input v-model="currentWorkspace.voiceId" clearable placeholder="选择音色或手动输入 voice_id" />
|
||||||
@input="currentWorkspace.voiceName = ''" />
|
|
||||||
<div v-if="currentWorkspace.voiceName" class="selected-voice-name">
|
<div v-if="currentWorkspace.voiceName" class="selected-voice-name">
|
||||||
{{ currentWorkspace.voiceName }}
|
{{ currentWorkspace.voiceName }}
|
||||||
<span>{{ currentWorkspace.voiceId }}</span>
|
<span>{{ currentWorkspace.voiceId }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="delivery-field">
|
||||||
|
<span class="delivery-field__label">音色名称</span>
|
||||||
|
<el-input v-model="currentWorkspace.voiceName" clearable placeholder="对应 audio_info.voice_name" />
|
||||||
|
</div>
|
||||||
<div class="delivery-field">
|
<div class="delivery-field">
|
||||||
<span class="delivery-field__label">音色操作</span>
|
<span class="delivery-field__label">音色操作</span>
|
||||||
<div class="voice-inline-actions">
|
<div class="voice-inline-actions">
|
||||||
@@ -376,25 +383,29 @@
|
|||||||
append-to-body destroy-on-close>
|
append-to-body destroy-on-close>
|
||||||
<div class="secret-status" :class="{ 'secret-status--valid': secretStatus?.valid }">
|
<div class="secret-status" :class="{ 'secret-status--valid': secretStatus?.valid }">
|
||||||
<span>{{ secretStatusText }}</span>
|
<span>{{ secretStatusText }}</span>
|
||||||
<span v-if="secretStatus?.expiresAt">过期时间:{{ formatSecretTime(secretStatus.expiresAt) }}</span>
|
<span v-if="secretExpireText">{{ secretExpireText }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form label-position="top" class="secret-form">
|
<el-form label-position="top" class="secret-form">
|
||||||
<el-form-item label="主工作流密钥">
|
<el-form-item label="爬虫密钥">
|
||||||
<el-input v-model="secretForm.copyApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
|
<el-input v-model="secretForm.copyApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
|
||||||
<div v-if="secretStatus?.copyApiKeyMasked" class="secret-form__hint">当前:{{ secretStatus.copyApiKeyMasked }}
|
<div v-if="secretStatus?.copyApiKeyMasked" class="secret-form__hint">当前:{{ secretStatus.copyApiKeyMasked }}
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="T8 平台密钥">
|
<el-form-item label="T8密钥(海外)">
|
||||||
<el-input v-model="secretForm.t8Key" type="password" show-password placeholder="不填写则保留当前有效值" />
|
<el-input v-model="secretForm.t8Key" type="password" show-password placeholder="不填写则保留当前有效值" />
|
||||||
<div v-if="secretStatus?.t8KeyMasked" class="secret-form__hint">当前:{{ secretStatus.t8KeyMasked }}</div>
|
<div v-if="secretStatus?.t8KeyMasked" class="secret-form__hint">当前:{{ secretStatus.t8KeyMasked }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="音色服务密钥">
|
<el-form-item label="T8密钥(国内)">
|
||||||
|
<el-input v-model="secretForm.t8VideoKey" type="password" show-password placeholder="不填写则保留当前有效值" />
|
||||||
|
<div v-if="secretStatus?.t8VideoKeyMasked" class="secret-form__hint">当前:{{ secretStatus.t8VideoKeyMasked }}</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="音频密钥">
|
||||||
<el-input v-model="secretForm.voiceApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
|
<el-input v-model="secretForm.voiceApiKey" type="password" show-password placeholder="不填写则保留当前有效值" />
|
||||||
<div v-if="secretStatus?.voiceApiKeyMasked" class="secret-form__hint">当前:{{ secretStatus.voiceApiKeyMasked }}
|
<div v-if="secretStatus?.voiceApiKeyMasked" class="secret-form__hint">当前:{{ secretStatus.voiceApiKeyMasked }}
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="音色分组 ID">
|
<el-form-item label="音频团队ID">
|
||||||
<el-input v-model="secretForm.voiceGroupId" type="password" show-password placeholder="不填写则保留当前有效值" />
|
<el-input v-model="secretForm.voiceGroupId" type="password" show-password placeholder="不填写则保留当前有效值" />
|
||||||
<div v-if="secretStatus?.voiceGroupIdMasked" class="secret-form__hint">当前:{{ secretStatus.voiceGroupIdMasked
|
<div v-if="secretStatus?.voiceGroupIdMasked" class="secret-form__hint">当前:{{ secretStatus.voiceGroupIdMasked
|
||||||
}}</div>
|
}}</div>
|
||||||
@@ -404,6 +415,7 @@
|
|||||||
<el-option label="1 天" :value="1" />
|
<el-option label="1 天" :value="1" />
|
||||||
<el-option label="7 天" :value="7" />
|
<el-option label="7 天" :value="7" />
|
||||||
<el-option label="30 天" :value="30" />
|
<el-option label="30 天" :value="30" />
|
||||||
|
<el-option label="永久" :value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -423,8 +435,8 @@
|
|||||||
<strong>{{ filteredVoiceListItems.length }}</strong>
|
<strong>{{ filteredVoiceListItems.length }}</strong>
|
||||||
<span>/ {{ voiceListItems.length }} 个音色</span>
|
<span>/ {{ voiceListItems.length }} 个音色</span>
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="voiceSearchKeyword" class="voice-search-input" clearable
|
<el-input v-model="voiceSearchKeyword" class="voice-search-input" clearable placeholder="搜索音色名称或 voice_id"
|
||||||
placeholder="搜索音色名称或 voice_id" @keyup.enter="listVoices" @clear="listVoices" />
|
@keyup.enter="listVoices" @clear="listVoices" />
|
||||||
<button type="button" class="script-primary-action-btn" :disabled="voiceLoading" @click="listVoices">
|
<button type="button" class="script-primary-action-btn" :disabled="voiceLoading" @click="listVoices">
|
||||||
刷新音色
|
刷新音色
|
||||||
</button>
|
</button>
|
||||||
@@ -438,8 +450,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="voice-list__source">{{ item.sourceLabel }}</span>
|
<span class="voice-list__source">{{ item.sourceLabel }}</span>
|
||||||
<button type="button" class="script-action-btn" @click="useVoice(item)">使用此音色</button>
|
<button type="button" class="script-action-btn" @click="useVoice(item)">使用此音色</button>
|
||||||
<button v-if="item.sourceKey !== 'system_voice'" type="button" class="script-action-btn script-action-btn--danger"
|
<button v-if="item.sourceKey !== 'system_voice'" type="button"
|
||||||
:disabled="voiceLoading" @click="deleteVoice(item)">删除音色</button>
|
class="script-action-btn script-action-btn--danger" :disabled="voiceLoading"
|
||||||
|
@click="deleteVoice(item)">删除音色</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂无可展示音色,请确认接口已返回音色数据" />
|
<el-empty v-else description="暂无可展示音色,请确认接口已返回音色数据" />
|
||||||
@@ -483,7 +496,7 @@ import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
|||||||
import {
|
import {
|
||||||
cloneImageVideoVoice,
|
cloneImageVideoVoice,
|
||||||
deleteImageVideoVoice,
|
deleteImageVideoVoice,
|
||||||
getImageVideoWorkflowResult,
|
getImageVideoAsyncTask,
|
||||||
getImageVideoSecretStatus,
|
getImageVideoSecretStatus,
|
||||||
listImageVideoVoices,
|
listImageVideoVoices,
|
||||||
runImageVideoDouyinCopy,
|
runImageVideoDouyinCopy,
|
||||||
@@ -492,6 +505,7 @@ import {
|
|||||||
synthesizeImageVideoVoice,
|
synthesizeImageVideoVoice,
|
||||||
uploadImageVideoMedia,
|
uploadImageVideoMedia,
|
||||||
type ImageVideoSecretStatusVo,
|
type ImageVideoSecretStatusVo,
|
||||||
|
type ImageVideoAsyncTaskVo,
|
||||||
type ImageVideoWorkflowParameters,
|
type ImageVideoWorkflowParameters,
|
||||||
} from '@/shared/api/java-modules'
|
} from '@/shared/api/java-modules'
|
||||||
|
|
||||||
@@ -502,6 +516,9 @@ type AssetKey = 'referenceAsset' | 'sceneAsset' | 'modelAsset' | 'speechAsset'
|
|||||||
type VoiceMode = 'textOnly' | 'uploadAudio' | 'synthesize'
|
type VoiceMode = 'textOnly' | 'uploadAudio' | 'synthesize'
|
||||||
type VoiceStatusType = 'info' | 'success' | 'error'
|
type VoiceStatusType = 'info' | 'success' | 'error'
|
||||||
|
|
||||||
|
const IMAGE_VIDEO_TASK_POLL_DELAY_MS = 5000
|
||||||
|
const IMAGE_VIDEO_TASK_MAX_POLLS = 720
|
||||||
|
|
||||||
type AssetState = {
|
type AssetState = {
|
||||||
fileName: string
|
fileName: string
|
||||||
previewUrl: string
|
previewUrl: string
|
||||||
@@ -612,6 +629,7 @@ const assemblyPrimaryActionText = computed(() => {
|
|||||||
const secretForm = reactive({
|
const secretForm = reactive({
|
||||||
copyApiKey: '',
|
copyApiKey: '',
|
||||||
t8Key: '',
|
t8Key: '',
|
||||||
|
t8VideoKey: '',
|
||||||
voiceApiKey: '',
|
voiceApiKey: '',
|
||||||
voiceGroupId: '',
|
voiceGroupId: '',
|
||||||
expireDays: 7,
|
expireDays: 7,
|
||||||
@@ -634,9 +652,9 @@ const audioTypeOptions = [
|
|||||||
{ label: '背景旁白', value: '2' },
|
{ label: '背景旁白', value: '2' },
|
||||||
]
|
]
|
||||||
const voiceModeOptions = [
|
const voiceModeOptions = [
|
||||||
{ label: '只提交文案', value: 'textOnly' },
|
{ label: '视频原声', value: 'textOnly' },
|
||||||
{ label: '上传/粘贴已有音频', value: 'uploadAudio' },
|
{ label: '背景音乐', value: 'uploadAudio' },
|
||||||
{ label: '用音色合成配音', value: 'synthesize' },
|
{ label: '参考音色', value: 'synthesize' },
|
||||||
]
|
]
|
||||||
const voiceCloneAsset = reactive<AssetState>(createAssetState())
|
const voiceCloneAsset = reactive<AssetState>(createAssetState())
|
||||||
|
|
||||||
@@ -716,6 +734,11 @@ const secretStatusText = computed(() => {
|
|||||||
if (secretStatus.value.valid) return '密钥已配置且有效'
|
if (secretStatus.value.valid) return '密钥已配置且有效'
|
||||||
return '密钥已过期或不可用'
|
return '密钥已过期或不可用'
|
||||||
})
|
})
|
||||||
|
const secretExpireText = computed(() => {
|
||||||
|
if (!secretStatus.value?.expiresAt) return ''
|
||||||
|
if (secretStatus.value.expireDays === 0) return '有效期:永久'
|
||||||
|
return `过期时间:${formatSecretTime(secretStatus.value.expiresAt)}`
|
||||||
|
})
|
||||||
|
|
||||||
function normalizeDuration(value: number | string) {
|
function normalizeDuration(value: number | string) {
|
||||||
const nextValue = Number(value)
|
const nextValue = Number(value)
|
||||||
@@ -913,9 +936,10 @@ function openSecretDialog() {
|
|||||||
function resetSecretForm() {
|
function resetSecretForm() {
|
||||||
secretForm.copyApiKey = ''
|
secretForm.copyApiKey = ''
|
||||||
secretForm.t8Key = ''
|
secretForm.t8Key = ''
|
||||||
|
secretForm.t8VideoKey = ''
|
||||||
secretForm.voiceApiKey = ''
|
secretForm.voiceApiKey = ''
|
||||||
secretForm.voiceGroupId = ''
|
secretForm.voiceGroupId = ''
|
||||||
secretForm.expireDays = secretStatus.value?.expireDays || 7
|
secretForm.expireDays = secretStatus.value?.expireDays ?? 7
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSecretSettings() {
|
async function saveSecretSettings() {
|
||||||
@@ -924,6 +948,7 @@ async function saveSecretSettings() {
|
|||||||
secretStatus.value = await saveImageVideoSecrets({
|
secretStatus.value = await saveImageVideoSecrets({
|
||||||
copyApiKey: secretForm.copyApiKey.trim(),
|
copyApiKey: secretForm.copyApiKey.trim(),
|
||||||
t8Key: secretForm.t8Key.trim(),
|
t8Key: secretForm.t8Key.trim(),
|
||||||
|
t8VideoKey: secretForm.t8VideoKey.trim(),
|
||||||
voiceApiKey: secretForm.voiceApiKey.trim(),
|
voiceApiKey: secretForm.voiceApiKey.trim(),
|
||||||
voiceGroupId: secretForm.voiceGroupId.trim(),
|
voiceGroupId: secretForm.voiceGroupId.trim(),
|
||||||
expireDays: secretForm.expireDays,
|
expireDays: secretForm.expireDays,
|
||||||
@@ -943,6 +968,42 @@ function formatSecretTime(value: string) {
|
|||||||
return value.replace('T', ' ').slice(0, 19)
|
return value.replace('T', ' ').slice(0, 19)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(milliseconds: number) {
|
||||||
|
return new Promise<void>((resolve) => window.setTimeout(resolve, milliseconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTaskStatus(value: string | undefined) {
|
||||||
|
return (value || '').trim().toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminalCozeStatus(value: string | undefined) {
|
||||||
|
return ['SUCCESS', 'SUCCEEDED', 'COMPLETED', 'DONE', 'FINISHED', 'FAILED', 'FAIL', 'ERROR', 'CANCELED', 'CANCELLED']
|
||||||
|
.includes(normalizeTaskStatus(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecoverableFailedImageVideoTask(task: ImageVideoAsyncTaskVo) {
|
||||||
|
return task.status === 'FAILED'
|
||||||
|
&& Boolean(task.cozeExecuteId)
|
||||||
|
&& !isTerminalCozeStatus(task.cozeStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTerminalImageVideoTask(task: ImageVideoAsyncTaskVo) {
|
||||||
|
return task.status === 'SUCCESS' || (task.status === 'FAILED' && !isRecoverableFailedImageVideoTask(task))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForImageVideoTask(ticket: ImageVideoAsyncTaskVo): Promise<unknown> {
|
||||||
|
let task = ticket
|
||||||
|
for (let pollCount = 0; pollCount < IMAGE_VIDEO_TASK_MAX_POLLS; pollCount += 1) {
|
||||||
|
if (task.status === 'SUCCESS') return task.result
|
||||||
|
if (isTerminalImageVideoTask(task)) {
|
||||||
|
throw new Error(task.errorMessage || 'Coze task failed')
|
||||||
|
}
|
||||||
|
await sleep(IMAGE_VIDEO_TASK_POLL_DELAY_MS)
|
||||||
|
task = await getImageVideoAsyncTask(task.taskId)
|
||||||
|
}
|
||||||
|
throw new Error('Coze task polling timed out')
|
||||||
|
}
|
||||||
|
|
||||||
async function rewriteScriptFromSource() {
|
async function rewriteScriptFromSource() {
|
||||||
const sourceUrl = currentWorkspace.value.scriptSourceUrl.trim()
|
const sourceUrl = currentWorkspace.value.scriptSourceUrl.trim()
|
||||||
const referenceUrl = currentWorkspace.value.referenceAsset.sourceUrl.trim()
|
const referenceUrl = currentWorkspace.value.referenceAsset.sourceUrl.trim()
|
||||||
@@ -958,7 +1019,7 @@ async function rewriteScriptFromSource() {
|
|||||||
if (activeTab.value === 'remake') currentWorkspace.value.scriptSourceUrl = url
|
if (activeTab.value === 'remake') currentWorkspace.value.scriptSourceUrl = url
|
||||||
copyLearningLoading.value = true
|
copyLearningLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await runImageVideoDouyinCopy({
|
const ticket = await runImageVideoDouyinCopy({
|
||||||
url,
|
url,
|
||||||
duration: Math.max(0, Math.round(Number(currentWorkspace.value.durationSeconds) || 0)),
|
duration: Math.max(0, Math.round(Number(currentWorkspace.value.durationSeconds) || 0)),
|
||||||
proc_info: {
|
proc_info: {
|
||||||
@@ -968,7 +1029,8 @@ async function rewriteScriptFromSource() {
|
|||||||
properties: currentWorkspace.value.productFocus.trim(),
|
properties: currentWorkspace.value.productFocus.trim(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const text = cleanCopyText(result.scriptDraft || result.recognizedContent || '')
|
const result = await waitForImageVideoTask(ticket)
|
||||||
|
const text = cleanCopyText(findTextByKeys(result, ['scriptDraft', 'script_draft', 'recognizedContent', 'recognized_content']))
|
||||||
currentWorkspace.value.scriptText = text
|
currentWorkspace.value.scriptText = text
|
||||||
ElMessage.success('文案已提取')
|
ElMessage.success('文案已提取')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1000,12 +1062,36 @@ function resolveProductImageUrls(workspace: WorkspaceState) {
|
|||||||
return workspace.productAssets.map(resolveAssetUrl).filter(Boolean).slice(0, 5)
|
return workspace.productAssets.map(resolveAssetUrl).filter(Boolean).slice(0, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSpeechFileUrl(workspace: WorkspaceState) {
|
function resolveVoiceAudioUrl(workspace: WorkspaceState) {
|
||||||
if (workspace.voiceMode === 'textOnly') return ''
|
if (workspace.voiceMode !== 'synthesize') return ''
|
||||||
const url = resolveAssetUrl(workspace.speechAsset)
|
const url = resolveAssetUrl(workspace.speechAsset)
|
||||||
return isCozeWorkflowDebugUrl(url) ? '' : url
|
return isCozeWorkflowDebugUrl(url) ? '' : url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveBgmUrl(workspace: WorkspaceState) {
|
||||||
|
if (workspace.voiceMode !== 'uploadAudio') return ''
|
||||||
|
const url = resolveAssetUrl(workspace.speechAsset)
|
||||||
|
return isCozeWorkflowDebugUrl(url) ? '' : url
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAudioMode(workspace: WorkspaceState) {
|
||||||
|
if (workspace.voiceMode === 'uploadAudio') return 2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAudioInfo(workspace: WorkspaceState): ImageVideoWorkflowParameters['audio_info'] {
|
||||||
|
const mode = resolveAudioMode(workspace)
|
||||||
|
const audioUrl = mode === 1 ? resolveVoiceAudioUrl(workspace) : ''
|
||||||
|
const bgmUrl = mode === 2 ? resolveBgmUrl(workspace) : ''
|
||||||
|
return {
|
||||||
|
audio_url: audioUrl,
|
||||||
|
type: Number(workspace.audioType) || 1,
|
||||||
|
bgm_url: bgmUrl,
|
||||||
|
mode,
|
||||||
|
voice_name: mode === 1 ? workspace.voiceName.trim() : '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveReferenceVideoInfo(workspace: WorkspaceState) {
|
function resolveReferenceVideoInfo(workspace: WorkspaceState) {
|
||||||
if (activeTab.value !== 'remake') {
|
if (activeTab.value !== 'remake') {
|
||||||
return { videoUrl: '', shareUrl: '', refVideoMode: '1' }
|
return { videoUrl: '', shareUrl: '', refVideoMode: '1' }
|
||||||
@@ -1041,6 +1127,7 @@ function buildImageVideoWorkflowParameters(): ImageVideoWorkflowParameters | nul
|
|||||||
return {
|
return {
|
||||||
api_key_info: {
|
api_key_info: {
|
||||||
t8star_key: '',
|
t8star_key: '',
|
||||||
|
t8_video_key: '',
|
||||||
ai_conductor_key: '',
|
ai_conductor_key: '',
|
||||||
},
|
},
|
||||||
bg_info: {
|
bg_info: {
|
||||||
@@ -1059,12 +1146,9 @@ function buildImageVideoWorkflowParameters(): ImageVideoWorkflowParameters | nul
|
|||||||
type: 1,
|
type: 1,
|
||||||
language: workspace.language,
|
language: workspace.language,
|
||||||
text: workspace.scriptText.trim(),
|
text: workspace.scriptText.trim(),
|
||||||
file_url: resolveSpeechFileUrl(workspace),
|
file_url: resolveVoiceAudioUrl(workspace),
|
||||||
},
|
|
||||||
audio_info: {
|
|
||||||
audio_url: resolveSpeechFileUrl(workspace),
|
|
||||||
type: Number(workspace.audioType) || 1,
|
|
||||||
},
|
},
|
||||||
|
audio_info: resolveAudioInfo(workspace),
|
||||||
video_info: {
|
video_info: {
|
||||||
video_url: referenceVideoInfo.videoUrl,
|
video_url: referenceVideoInfo.videoUrl,
|
||||||
share_url: referenceVideoInfo.shareUrl,
|
share_url: referenceVideoInfo.shareUrl,
|
||||||
@@ -1093,14 +1177,9 @@ async function startAssembly() {
|
|||||||
assembly.videoUrl = ''
|
assembly.videoUrl = ''
|
||||||
assembly.debugUrl = ''
|
assembly.debugUrl = ''
|
||||||
try {
|
try {
|
||||||
const result = await runImageVideoWorkflow(parameters)
|
const ticket = await runImageVideoWorkflow(parameters)
|
||||||
assembly.resultText = JSON.stringify(result, null, 2)
|
assembly.resultText = JSON.stringify(ticket, null, 2)
|
||||||
assembly.debugUrl = findTextByKeys(result, ['debug_url', 'debugUrl'])
|
startAssemblyPolling(tab, ticket.taskId)
|
||||||
const executeId = findTextByKeys(result, ['execute_id', 'executeId'])
|
|
||||||
if (executeId) {
|
|
||||||
assembly.executeId = executeId
|
|
||||||
startAssemblyPolling(tab, executeId)
|
|
||||||
}
|
|
||||||
ElMessage.success('工作流已提交')
|
ElMessage.success('工作流已提交')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '工作流提交失败')
|
ElMessage.error(error instanceof Error ? error.message : '工作流提交失败')
|
||||||
@@ -1149,12 +1228,12 @@ async function downloadAssemblyVideo() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAssemblyPolling(tab: WorkspaceTab, executeId: string) {
|
function startAssemblyPolling(tab: WorkspaceTab, taskId: number) {
|
||||||
stopAssemblyPolling(tab)
|
stopAssemblyPolling(tab)
|
||||||
const assembly = workspaces[tab].assembly
|
const assembly = workspaces[tab].assembly
|
||||||
assembly.polling = true
|
assembly.polling = true
|
||||||
assembly.pollCount = 0
|
assembly.pollCount = 0
|
||||||
void pollAssemblyResult(tab, executeId)
|
void pollAssemblyResult(tab, taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopAssemblyPolling(tab: WorkspaceTab) {
|
function stopAssemblyPolling(tab: WorkspaceTab) {
|
||||||
@@ -1167,28 +1246,31 @@ function stopAssemblyPolling(tab: WorkspaceTab) {
|
|||||||
assembly.pollCount = 0
|
assembly.pollCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollAssemblyResult(tab: WorkspaceTab, executeId: string) {
|
async function pollAssemblyResult(tab: WorkspaceTab, taskId: number) {
|
||||||
if (!executeId) return
|
if (!taskId) return
|
||||||
const assembly = workspaces[tab].assembly
|
const assembly = workspaces[tab].assembly
|
||||||
assembly.pollCount += 1
|
assembly.pollCount += 1
|
||||||
try {
|
try {
|
||||||
const result = await getImageVideoWorkflowResult(executeId)
|
const task = await getImageVideoAsyncTask(taskId)
|
||||||
const status = resolveWorkflowStatus(result)
|
const result = task.result
|
||||||
|
const status = task.cozeStatus || task.status
|
||||||
const videoUrl = findVideoResultUrl(result)
|
const videoUrl = findVideoResultUrl(result)
|
||||||
const debugUrl = findTextByKeys(result, ['debug_url', 'debugUrl'])
|
const debugUrl = findTextByKeys(result, ['debug_url', 'debugUrl'])
|
||||||
|
if (task.cozeExecuteId) assembly.executeId = task.cozeExecuteId
|
||||||
if (videoUrl) assembly.videoUrl = videoUrl
|
if (videoUrl) assembly.videoUrl = videoUrl
|
||||||
if (debugUrl) assembly.debugUrl = debugUrl
|
if (debugUrl) assembly.debugUrl = debugUrl
|
||||||
assembly.resultText = JSON.stringify({
|
assembly.resultText = JSON.stringify({
|
||||||
execute_id: executeId,
|
task_id: taskId,
|
||||||
polling: !isTerminalWorkflowStatus(status),
|
execute_id: task.cozeExecuteId || assembly.executeId,
|
||||||
|
polling: !isTerminalImageVideoTask(task),
|
||||||
poll_count: assembly.pollCount,
|
poll_count: assembly.pollCount,
|
||||||
status,
|
status,
|
||||||
video_url: videoUrl || assembly.videoUrl,
|
video_url: videoUrl || assembly.videoUrl,
|
||||||
result,
|
result,
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
if (isTerminalWorkflowStatus(status)) {
|
if (isTerminalImageVideoTask(task)) {
|
||||||
assembly.polling = false
|
assembly.polling = false
|
||||||
if (isFailedWorkflowStatus(status)) {
|
if (task.status === 'FAILED') {
|
||||||
ElMessage.error('Coze 工作流执行失败')
|
ElMessage.error('Coze 工作流执行失败')
|
||||||
} else {
|
} else {
|
||||||
ElMessage.success(videoUrl || assembly.videoUrl ? '视频生成完成' : 'Coze 工作流执行完成,未解析到视频地址')
|
ElMessage.success(videoUrl || assembly.videoUrl ? '视频生成完成' : 'Coze 工作流执行完成,未解析到视频地址')
|
||||||
@@ -1197,7 +1279,7 @@ async function pollAssemblyResult(tab: WorkspaceTab, executeId: string) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assembly.resultText = JSON.stringify({
|
assembly.resultText = JSON.stringify({
|
||||||
execute_id: executeId,
|
task_id: taskId,
|
||||||
polling: false,
|
polling: false,
|
||||||
error: error instanceof Error ? error.message : '查询 Coze 执行结果失败',
|
error: error instanceof Error ? error.message : '查询 Coze 执行结果失败',
|
||||||
}, null, 2)
|
}, null, 2)
|
||||||
@@ -1212,8 +1294,8 @@ async function pollAssemblyResult(tab: WorkspaceTab, executeId: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
assembly.pollTimer = window.setTimeout(() => {
|
assembly.pollTimer = window.setTimeout(() => {
|
||||||
void pollAssemblyResult(tab, executeId)
|
void pollAssemblyResult(tab, taskId)
|
||||||
}, 5000)
|
}, IMAGE_VIDEO_TASK_POLL_DELAY_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVoiceStatus(message: string, type: VoiceStatusType = 'info') {
|
function setVoiceStatus(message: string, type: VoiceStatusType = 'info') {
|
||||||
@@ -1548,7 +1630,8 @@ function firstString(...values: unknown[]) {
|
|||||||
async function listVoices() {
|
async function listVoices() {
|
||||||
voiceLoading.value = true
|
voiceLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await listImageVideoVoices(voiceSearchKeyword.value.trim())
|
const ticket = await listImageVideoVoices(voiceSearchKeyword.value.trim())
|
||||||
|
const result = await waitForImageVideoTask(ticket)
|
||||||
voiceListItems.value = extractVoiceItems(result)
|
voiceListItems.value = extractVoiceItems(result)
|
||||||
setVoiceStatus(
|
setVoiceStatus(
|
||||||
voiceListItems.value.length ? `已获取 ${voiceListItems.value.length} 个音色` : '音色接口已返回,未解析到列表项',
|
voiceListItems.value.length ? `已获取 ${voiceListItems.value.length} 个音色` : '音色接口已返回,未解析到列表项',
|
||||||
@@ -1580,7 +1663,8 @@ async function deleteVoice(item: VoiceListItem) {
|
|||||||
}
|
}
|
||||||
voiceLoading.value = true
|
voiceLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await deleteImageVideoVoice(voiceId)
|
const ticket = await deleteImageVideoVoice(voiceId)
|
||||||
|
await waitForImageVideoTask(ticket)
|
||||||
voiceListItems.value = voiceListItems.value.filter((item) => item.voiceId !== voiceId)
|
voiceListItems.value = voiceListItems.value.filter((item) => item.voiceId !== voiceId)
|
||||||
setVoiceStatus(`删除音色请求已提交:${voiceId}`, 'success')
|
setVoiceStatus(`删除音色请求已提交:${voiceId}`, 'success')
|
||||||
ElMessage.success('删除音色请求已提交')
|
ElMessage.success('删除音色请求已提交')
|
||||||
@@ -1610,15 +1694,16 @@ async function cloneVoice() {
|
|||||||
audioUrl: sourceUrl,
|
audioUrl: sourceUrl,
|
||||||
videoUrl: voiceCloneAsset.mediaType === 'video' ? sourceUrl : '',
|
videoUrl: voiceCloneAsset.mediaType === 'video' ? sourceUrl : '',
|
||||||
}
|
}
|
||||||
const result = await cloneImageVideoVoice({
|
const ticket = await cloneImageVideoVoice({
|
||||||
name,
|
name,
|
||||||
audioUrl: clonePayload.audioUrl,
|
audioUrl: clonePayload.audioUrl,
|
||||||
videoUrl: clonePayload.videoUrl,
|
videoUrl: clonePayload.videoUrl,
|
||||||
})
|
})
|
||||||
|
const result = await waitForImageVideoTask(ticket)
|
||||||
const voiceId = findTextByKeys(result, ['voice_id', 'voiceId', 'voiceID'])
|
const voiceId = findTextByKeys(result, ['voice_id', 'voiceId', 'voiceID'])
|
||||||
if (voiceId) {
|
if (voiceId) {
|
||||||
currentWorkspace.value.voiceId = voiceId
|
currentWorkspace.value.voiceId = voiceId
|
||||||
currentWorkspace.value.voiceName = ''
|
currentWorkspace.value.voiceName = name
|
||||||
setVoiceStatus(`音色克隆完成,已回填 voice_id:${voiceId}`, 'success')
|
setVoiceStatus(`音色克隆完成,已回填 voice_id:${voiceId}`, 'success')
|
||||||
ElMessage.success('音色克隆完成,已回填 voice_id')
|
ElMessage.success('音色克隆完成,已回填 voice_id')
|
||||||
} else {
|
} else {
|
||||||
@@ -1648,7 +1733,8 @@ async function synthesizeVoice() {
|
|||||||
}
|
}
|
||||||
voiceLoading.value = true
|
voiceLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = await synthesizeImageVideoVoice({ text, voiceId })
|
const ticket = await synthesizeImageVideoVoice({ text, voiceId })
|
||||||
|
const result = await waitForImageVideoTask(ticket)
|
||||||
const audioUrl = findMediaFileUrl(result)
|
const audioUrl = findMediaFileUrl(result)
|
||||||
if (audioUrl) {
|
if (audioUrl) {
|
||||||
setAssetUrl(activeTab.value, 'speechAsset', audioUrl, 'audio/*')
|
setAssetUrl(activeTab.value, 'speechAsset', audioUrl, 'audio/*')
|
||||||
|
|||||||
@@ -2123,7 +2123,9 @@ export interface PriceTrackAsinParsedRow {
|
|||||||
shippingFee?: string;
|
shippingFee?: string;
|
||||||
minimumPrice?: string;
|
minimumPrice?: string;
|
||||||
firstPlace?: string;
|
firstPlace?: string;
|
||||||
|
firstShop?: string;
|
||||||
secondPlace?: string;
|
secondPlace?: string;
|
||||||
|
secondShop?: string;
|
||||||
cartShopName?: string;
|
cartShopName?: string;
|
||||||
priceChangeStatus?: string;
|
priceChangeStatus?: string;
|
||||||
modifyCount?: string;
|
modifyCount?: string;
|
||||||
@@ -2417,6 +2419,10 @@ export function getTaskSkipPriceAsinsPaginated(
|
|||||||
taskId: number,
|
taskId: number,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
pageSize: number = 1000,
|
pageSize: number = 1000,
|
||||||
|
options: {
|
||||||
|
shopName?: string | string[];
|
||||||
|
countryCode?: string | string[];
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
get<JavaApiResponse<SkipPriceAsinPageVo>>(
|
get<JavaApiResponse<SkipPriceAsinPageVo>>(
|
||||||
@@ -2425,6 +2431,8 @@ export function getTaskSkipPriceAsinsPaginated(
|
|||||||
params: {
|
params: {
|
||||||
page,
|
page,
|
||||||
page_size: pageSize,
|
page_size: pageSize,
|
||||||
|
...(options.shopName ? { shop_name: options.shopName } : {}),
|
||||||
|
...(options.countryCode ? { country_code: options.countryCode } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -2503,10 +2511,12 @@ export interface ImageVideoSecretStatusVo {
|
|||||||
expired?: boolean;
|
expired?: boolean;
|
||||||
hasCopyApiKey?: boolean;
|
hasCopyApiKey?: boolean;
|
||||||
hasT8Key?: boolean;
|
hasT8Key?: boolean;
|
||||||
|
hasT8VideoKey?: boolean;
|
||||||
hasVoiceApiKey?: boolean;
|
hasVoiceApiKey?: boolean;
|
||||||
hasVoiceGroupId?: boolean;
|
hasVoiceGroupId?: boolean;
|
||||||
copyApiKeyMasked?: string;
|
copyApiKeyMasked?: string;
|
||||||
t8KeyMasked?: string;
|
t8KeyMasked?: string;
|
||||||
|
t8VideoKeyMasked?: string;
|
||||||
voiceApiKeyMasked?: string;
|
voiceApiKeyMasked?: string;
|
||||||
voiceGroupIdMasked?: string;
|
voiceGroupIdMasked?: string;
|
||||||
expireDays?: number;
|
expireDays?: number;
|
||||||
@@ -2516,6 +2526,7 @@ export interface ImageVideoSecretStatusVo {
|
|||||||
export interface ImageVideoSecretSavePayload {
|
export interface ImageVideoSecretSavePayload {
|
||||||
copyApiKey?: string;
|
copyApiKey?: string;
|
||||||
t8Key?: string;
|
t8Key?: string;
|
||||||
|
t8VideoKey?: string;
|
||||||
voiceApiKey?: string;
|
voiceApiKey?: string;
|
||||||
voiceGroupId?: string;
|
voiceGroupId?: string;
|
||||||
expireDays: number;
|
expireDays: number;
|
||||||
@@ -2524,6 +2535,7 @@ export interface ImageVideoSecretSavePayload {
|
|||||||
export interface ImageVideoWorkflowParameters {
|
export interface ImageVideoWorkflowParameters {
|
||||||
api_key_info: {
|
api_key_info: {
|
||||||
t8star_key: string;
|
t8star_key: string;
|
||||||
|
t8_video_key: string;
|
||||||
ai_conductor_key: string;
|
ai_conductor_key: string;
|
||||||
};
|
};
|
||||||
bg_info: {
|
bg_info: {
|
||||||
@@ -2551,6 +2563,9 @@ export interface ImageVideoWorkflowParameters {
|
|||||||
audio_info: {
|
audio_info: {
|
||||||
audio_url: string;
|
audio_url: string;
|
||||||
type: number;
|
type: number;
|
||||||
|
bgm_url: string;
|
||||||
|
mode: number;
|
||||||
|
voice_name: string;
|
||||||
};
|
};
|
||||||
video_info: {
|
video_info: {
|
||||||
video_url: string;
|
video_url: string;
|
||||||
@@ -2575,6 +2590,20 @@ export interface ImageVideoWorkflowResponse {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ImageVideoAsyncTaskStatus = 'PENDING' | 'RUNNING' | 'WAITING' | 'POLLING' | 'SUCCESS' | 'FAILED'
|
||||||
|
|
||||||
|
export interface ImageVideoAsyncTaskVo {
|
||||||
|
taskId: number;
|
||||||
|
taskType: string;
|
||||||
|
status: ImageVideoAsyncTaskStatus;
|
||||||
|
cozeExecuteId?: string;
|
||||||
|
cozeStatus?: string;
|
||||||
|
result?: unknown;
|
||||||
|
errorMessage?: string;
|
||||||
|
submittedAt?: string;
|
||||||
|
completedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImageVideoMediaUploadVo {
|
export interface ImageVideoMediaUploadVo {
|
||||||
url: string;
|
url: string;
|
||||||
objectKey: string;
|
objectKey: string;
|
||||||
@@ -2621,30 +2650,36 @@ export function saveImageVideoSecrets(payload: ImageVideoSecretSavePayload) {
|
|||||||
|
|
||||||
export function runImageVideoDouyinCopy(payload: ImageVideoDouyinCopyPayload) {
|
export function runImageVideoDouyinCopy(payload: ImageVideoDouyinCopyPayload) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoDouyinCopyVo>, ImageVideoDouyinCopyPayload & { userId: number }>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, ImageVideoDouyinCopyPayload & { userId: number }>(
|
||||||
`${JAVA_API_PREFIX}/image-video/douyin-copy`,
|
`${JAVA_API_PREFIX}/image-video/douyin-copy`,
|
||||||
{ userId: getCurrentUserId(), ...payload },
|
{ userId: getCurrentUserId(), ...payload },
|
||||||
{ timeout: 180000 },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runImageVideoWorkflow(parameters: ImageVideoWorkflowParameters) {
|
export function runImageVideoWorkflow(parameters: ImageVideoWorkflowParameters) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoWorkflowResponse>, ImageVideoWorkflowRunRequest>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, ImageVideoWorkflowRunRequest>(
|
||||||
`${JAVA_API_PREFIX}/image-video/workflow/run`,
|
`${JAVA_API_PREFIX}/image-video/workflow/run`,
|
||||||
{ userId: getCurrentUserId(), parameters },
|
{ userId: getCurrentUserId(), parameters },
|
||||||
{ timeout: 3600000 },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageVideoWorkflowResult(executeId: string) {
|
export function getImageVideoWorkflowResult(executeId: string) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoWorkflowResponse>, ImageVideoWorkflowResultRequest>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, ImageVideoWorkflowResultRequest>(
|
||||||
`${JAVA_API_PREFIX}/image-video/workflow/result`,
|
`${JAVA_API_PREFIX}/image-video/workflow/result`,
|
||||||
{ userId: getCurrentUserId(), executeId },
|
{ userId: getCurrentUserId(), executeId },
|
||||||
{ timeout: 3600000 },
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageVideoAsyncTask(taskId: number) {
|
||||||
|
return unwrapJavaResponse(
|
||||||
|
get<JavaApiResponse<ImageVideoAsyncTaskVo>>(
|
||||||
|
`${JAVA_API_PREFIX}/image-video/tasks/${taskId}`,
|
||||||
|
{ params: { user_id: getCurrentUserId() } },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2680,40 +2715,36 @@ function resolveImageVideoMediaType(file: File) {
|
|||||||
|
|
||||||
export function listImageVideoVoices(name = "") {
|
export function listImageVideoVoices(name = "") {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; name: string }>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; name: string }>(
|
||||||
`${JAVA_API_PREFIX}/image-video/voice/list`,
|
`${JAVA_API_PREFIX}/image-video/voice/list`,
|
||||||
{ userId: getCurrentUserId(), name },
|
{ userId: getCurrentUserId(), name },
|
||||||
{ timeout: 180000 },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteImageVideoVoice(voiceId: string) {
|
export function deleteImageVideoVoice(voiceId: string) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; voiceId: string }>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; voiceId: string }>(
|
||||||
`${JAVA_API_PREFIX}/image-video/voice/delete`,
|
`${JAVA_API_PREFIX}/image-video/voice/delete`,
|
||||||
{ userId: getCurrentUserId(), voiceId },
|
{ userId: getCurrentUserId(), voiceId },
|
||||||
{ timeout: 180000 },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneImageVideoVoice(payload: { name?: string; audioUrl?: string; videoUrl?: string }) {
|
export function cloneImageVideoVoice(payload: { name?: string; audioUrl?: string; videoUrl?: string }) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; name?: string; audioUrl?: string; videoUrl?: string }>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; name?: string; audioUrl?: string; videoUrl?: string }>(
|
||||||
`${JAVA_API_PREFIX}/image-video/voice/clone`,
|
`${JAVA_API_PREFIX}/image-video/voice/clone`,
|
||||||
{ userId: getCurrentUserId(), ...payload },
|
{ userId: getCurrentUserId(), ...payload },
|
||||||
{ timeout: 180000 },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function synthesizeImageVideoVoice(payload: { text: string; voiceId: string }) {
|
export function synthesizeImageVideoVoice(payload: { text: string; voiceId: string }) {
|
||||||
return unwrapJavaResponse(
|
return unwrapJavaResponse(
|
||||||
post<JavaApiResponse<ImageVideoVoiceWorkflowResponse>, { userId: number; text: string; voiceId: string }>(
|
post<JavaApiResponse<ImageVideoAsyncTaskVo>, { userId: number; text: string; voiceId: string }>(
|
||||||
`${JAVA_API_PREFIX}/image-video/voice/synthesis`,
|
`${JAVA_API_PREFIX}/image-video/voice/synthesis`,
|
||||||
{ userId: getCurrentUserId(), ...payload },
|
{ userId: getCurrentUserId(), ...payload },
|
||||||
{ timeout: 180000 },
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user