mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
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:
@@ -14,7 +14,7 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>3.0.0</revision>
|
||||
<revision>3.1.0</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 是否启用工作流
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -42,4 +42,9 @@ public class ImageContext {
|
||||
@Min(value = 0, message = "随机数种子不能小于0")
|
||||
@Max(value = 2147483647, message = "随机数种子不能大于2147483647")
|
||||
private Integer seed;
|
||||
|
||||
/**
|
||||
* 参考图 URL(图生图用)
|
||||
*/
|
||||
private String image;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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("当前图片供应商不支持临时媒体上传");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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() + "]");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user