feat: 新增短剧/媒体生成能力并脱敏数据库脚本

- 新增音频/视频/图片生成服务与 Atlas、OpenAI 实现
- 新增短剧脚本生成、分镜与 ffmpeg 合成模块
- 新增 Coze/Dify 聊天 provider 及 Agent 配置
- 脱敏 SQL 脚本:移除真实 DeepSeek API Key,掩码 COS 桶名
- 清理过期的 SQL update 脚本与 chat 文档

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ageerle@163.com
2026-07-14 21:49:03 +08:00
parent 7aa67cfc2f
commit 2ae13aafd4
132 changed files with 11949 additions and 977 deletions

View File

@@ -14,7 +14,7 @@
</description>
<properties>
<revision>3.0.0</revision>
<revision>3.1.0</revision>
</properties>
<dependencyManagement>

View File

@@ -21,9 +21,19 @@ import java.util.List;
@Data
public class ChatRequest {
@NotEmpty(message = "传入的模型不能为空")
/**
* 模型名称。
* 智能体模式下可缺省:传 agentId 时后端按智能体绑定的模型解析,此字段仅作回退。
*/
private String model;
/**
* 智能体ID。传入时后端按智能体配置解析模型/工具/技能/知识库/提示词/是否深度思考。
*/
@JsonSerialize(using = ToStringSerializer.class)
@JSONField(serializeUsing = String.class)
private Long agentId;
/**
* 对话消息
*/
@@ -35,16 +45,6 @@ public class ChatRequest {
*/
private WorkFlowRunner workFlowRunner;
/**
* 人机交互信息体
*/
private ReSumeRunner reSumeRunner;
/**
* 是否为人机交互用户继续输入
*/
private Boolean isResume = false;
/**
* 是否启用工作流
*/

View File

@@ -1,19 +0,0 @@
package org.ruoyi.common.chat.domain.dto.request;
import lombok.Data;
/**
* 人机交互输入信息
*/
@Data
public class ReSumeRunner {
/**
* 运行节点UUID
*/
private String runtimeUuid;
/**
* 人机交互用户输入信息
*/
private String feedbackContent;
}

View File

@@ -0,0 +1,29 @@
package org.ruoyi.common.chat.entity.audio;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/**
* Text-to-speech context.
*/
@Data
@Builder
public class AudioContext {
@NotNull(message = "模型配置不能为空")
private ChatModelVo chatModelVo;
@NotBlank(message = "输入文本不能为空")
private String input;
private String voice;
private String responseFormat;
private Double speed;
private String instructions;
}

View File

@@ -42,4 +42,9 @@ public class ImageContext {
@Min(value = 0, message = "随机数种子不能小于0")
@Max(value = 2147483647, message = "随机数种子不能大于2147483647")
private Integer seed;
/**
* 参考图 URL图生图用
*/
private String image;
}

View File

@@ -0,0 +1,28 @@
package org.ruoyi.common.chat.entity.media;
import lombok.Builder;
import lombok.Data;
/**
* Unified media generation response.
*/
@Data
@Builder
public class MediaGenerationResponse {
private String type;
private String mimeType;
private String url;
private String b64Json;
private String dataUrl;
private String id;
private String status;
private String rawResponse;
}

View File

@@ -0,0 +1,35 @@
package org.ruoyi.common.chat.entity.video;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/**
* Text-to-video context.
*/
@Data
@Builder
public class VideoContext {
@NotNull(message = "模型配置不能为空")
private ChatModelVo chatModelVo;
@NotBlank(message = "提示词不能为空")
private String prompt;
private String size;
private Integer seconds;
private String quality;
/** 参考图 URL图生视频模式单图 */
private String imageUrl;
/** 多参考图 URL多参考图生视频模式配合 @imageN prompt 使用) */
private java.util.List<String> referenceImages;
private String videoId;
}

View File

@@ -0,0 +1,40 @@
package org.ruoyi.common.chat.factory;
import org.ruoyi.common.chat.service.audio.IAudioGenerationService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Text-to-speech service factory.
*/
@Component
public class AudioServiceFactory implements ApplicationContextAware {
private final Map<String, IAudioGenerationService> audioServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IAudioGenerationService> serviceMap = applicationContext.getBeansOfType(IAudioGenerationService.class);
for (IAudioGenerationService service : serviceMap.values()) {
if (service != null) {
audioServiceMap.put(service.getProviderName(), service);
}
}
}
public IAudioGenerationService getOriginalService(String providerCode) {
IAudioGenerationService service = audioServiceMap.get(providerCode);
if (service == null && "custom_api".equals(providerCode)) {
service = audioServiceMap.get("openai");
}
if (service == null) {
throw new IllegalArgumentException("不支持的语音模型供应商: " + providerCode);
}
return service;
}
}

View File

@@ -37,6 +37,9 @@ public class ImageServiceFactory implements ApplicationContextAware {
*/
public IImageGenerationService getOriginalService(String category) {
IImageGenerationService service = imageSerivceMap.get(category);
if (service == null && "custom_api".equals(category)) {
service = imageSerivceMap.get("openai");
}
if (service == null) {
throw new IllegalArgumentException("不支持的模型类别: " + category);
}

View File

@@ -0,0 +1,40 @@
package org.ruoyi.common.chat.factory;
import org.ruoyi.common.chat.service.video.IVideoGenerationService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Text-to-video service factory.
*/
@Component
public class VideoServiceFactory implements ApplicationContextAware {
private final Map<String, IVideoGenerationService> videoServiceMap = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, IVideoGenerationService> serviceMap = applicationContext.getBeansOfType(IVideoGenerationService.class);
for (IVideoGenerationService service : serviceMap.values()) {
if (service != null) {
videoServiceMap.put(service.getProviderName(), service);
}
}
}
public IVideoGenerationService getOriginalService(String providerCode) {
IVideoGenerationService service = videoServiceMap.get(providerCode);
if (service == null && "custom_api".equals(providerCode)) {
service = videoServiceMap.get("openai");
}
if (service == null) {
throw new IllegalArgumentException("不支持的视频模型供应商: " + providerCode);
}
return service;
}
}

View File

@@ -0,0 +1,15 @@
package org.ruoyi.common.chat.service.audio;
import jakarta.validation.Valid;
import org.ruoyi.common.chat.entity.audio.AudioContext;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
/**
* Public text-to-speech service.
*/
public interface IAudioGenerationService {
MediaGenerationResponse generateSpeech(@Valid AudioContext audioContext);
String getProviderName();
}

View File

@@ -2,6 +2,8 @@ package org.ruoyi.common.chat.service.image;
import jakarta.validation.Valid;
import org.ruoyi.common.chat.entity.image.ImageContext;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
/**
* 公共文生图接口
@@ -9,10 +11,31 @@ import org.ruoyi.common.chat.entity.image.ImageContext;
public interface IImageGenerationService {
/**
* 根据文字生成图片
* 根据文字生成图片(同步阻塞,返回 URL
*/
String generateImage(@Valid ImageContext imageContext);
/**
* 异步启动图片生成(返回 prediction 信息,用于轮询进度)
* 默认回退到同步模式
*/
default MediaGenerationResponse startImageGeneration(@Valid ImageContext imageContext) {
String url = generateImage(imageContext);
return MediaGenerationResponse.builder()
.type("image")
.url(url)
.status("completed")
.build();
}
/**
* 上传图生图/图生视频使用的临时媒体文件。
* 不支持上传的供应商保持默认异常,避免静默回退到错误存储。
*/
default String uploadMedia(ChatModelVo model, byte[] content, String fileName, String contentType) {
throw new UnsupportedOperationException("当前图片供应商不支持临时媒体上传");
}
/**
* 获取服务提供商名称
*/

View File

@@ -0,0 +1,17 @@
package org.ruoyi.common.chat.service.video;
import jakarta.validation.Valid;
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
import org.ruoyi.common.chat.entity.video.VideoContext;
/**
* Public text-to-video service.
*/
public interface IVideoGenerationService {
MediaGenerationResponse generateVideo(@Valid VideoContext videoContext);
MediaGenerationResponse retrieveVideo(@Valid VideoContext videoContext);
String getProviderName();
}

View File

@@ -1,8 +1,11 @@
package org.ruoyi.common.core.service;
import jakarta.servlet.http.HttpServletResponse;
import org.ruoyi.common.core.domain.dto.OssDTO;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
@@ -33,5 +36,19 @@ public interface OssService {
*/
OssDTO uploadFile(MultipartFile file);
/**
* Upload a server-side file without buffering the complete payload in memory.
*/
OssDTO uploadFile(File file);
/**
* Stream an object as an authenticated attachment response.
*/
void downloadFile(Long ossId, HttpServletResponse response) throws IOException;
/**
* Delete one object and its sys_oss metadata.
*/
Boolean deleteFile(Long ossId);
}

View File

@@ -310,7 +310,7 @@ public class OssClient {
client.deleteObject(
x -> x.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build());
.build()).join();
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}