mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-09-13 00:14:59 +00:00
Compare commits
2 Commits
7aa67cfc2f
...
efc30c3ac8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efc30c3ac8 | ||
|
|
2ae13aafd4 |
15
README.md
15
README.md
@@ -29,13 +29,13 @@
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:----------:|---
|
||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱/MiniMax)、多模态理解、Coze/DIFY/FastGPT平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
||||
| 模块 | 现有能力
|
||||
|:---------:|---
|
||||
| **模型管理** | 多模型接入(DeepSeek/智谱/MIMO/百炼/OpenAI)、多模态理解、Coze/DIFY/FastGPT/RAGFlow平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **智能体管理** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型,可以灵活搭配工具,skills
|
||||
|
||||
|
||||
### 项目源码
|
||||
@@ -45,6 +45,7 @@
|
||||
| 🔧 后端服务 | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 用户前端 | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ 管理后台 | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 短剧服务 | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | |
|
||||
|
||||
### 合作项目
|
||||
| 项目名称 | GitHub 仓库 | Gitee 仓库
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
| 🔧 Backend | [ruoyi-ai](https://github.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitee.com/ageerle/ruoyi-ai) | [ruoyi-ai](https://gitcode.com/ageerle/ruoyi-ai) |
|
||||
| 🎨 User Frontend | [ruoyi-web](https://github.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitee.com/ageerle/ruoyi-web) | [ruoyi-web](https://gitcode.com/ageerle/ruoyi-web) |
|
||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
| 🎬 Drama | [ruoyi-drama](https://github.com/ageerle/ruoyi-drama) | [ruoyi-drama](https://gitee.com/ageerle/ruoyi-drama) | |
|
||||
|
||||
### Partner Projects
|
||||
| Project Name | GitHub Repository | Gitee Repository |
|
||||
|
||||
@@ -20,6 +20,22 @@ RUN mvn clean package -Pprod -DskipTests
|
||||
# 最终运行镜像
|
||||
FROM eclipse-temurin:17-jre-alpine
|
||||
|
||||
RUN set -eux; \
|
||||
apk add --no-cache ffmpeg; \
|
||||
for filter in scale pad fps setsar format tpad trim settb setpts xfade \
|
||||
aresample aformat apad atrim asetpts anullsrc concat acrossfade; do \
|
||||
ffmpeg -hide_banner -filters 2>/dev/null | grep -Eq "[[:space:]]${filter}[[:space:]]"; \
|
||||
done; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q dissolve; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q fadeblack; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q slideleft; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]libx264[[:space:]]'; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]aac[[:space:]]'; \
|
||||
ffprobe -hide_banner -version >/dev/null
|
||||
|
||||
ENV FFMPEG_PATH=/usr/bin/ffmpeg \
|
||||
FFPROBE_PATH=/usr/bin/ffprobe
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
@@ -27,10 +43,10 @@ WORKDIR /app
|
||||
COPY --from=builder /build/ruoyi-admin/target/ruoyi-admin.jar ./ruoyi-admin.jar
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p /ruoyi/server/logs
|
||||
RUN mkdir -p /ruoyi/server/logs /ruoyi/server/temp
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 6039
|
||||
|
||||
# 启动命令
|
||||
ENTRYPOINT ["java", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"]
|
||||
ENTRYPOINT ["java", "-Djava.io.tmpdir=/ruoyi/server/temp", "-jar", "ruoyi-admin.jar", "--spring.profiles.active=prod"]
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 数据库初始化脚本
|
||||
# 使用 --force 参数确保即使出错也继续执行
|
||||
|
||||
echo "开始初始化数据库..."
|
||||
|
||||
# 使用 --force 参数忽略错误继续执行
|
||||
mysql -uroot -proot ruoyi-ai-agent --force < /docker-entrypoint-initdb.d/ruoyi-ai-v3_mysql8.sql
|
||||
|
||||
echo "数据库初始化完成"
|
||||
86
docs/script/install-ffmpeg-windows.ps1
Normal file
86
docs/script/install-ffmpeg-windows.ps1
Normal file
@@ -0,0 +1,86 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Find-Executable([string]$Name) {
|
||||
$command = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($command) {
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
$roots = @(
|
||||
(Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages'),
|
||||
(Join-Path $env:USERPROFILE 'scoop\apps\ffmpeg'),
|
||||
'C:\ProgramData\chocolatey\bin',
|
||||
'C:\ffmpeg'
|
||||
)
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
$match = Get-ChildItem -Path $root -Filter "$Name.exe" -Recurse -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1 -ExpandProperty FullName
|
||||
if ($match) { return $match }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$ffmpeg = Find-Executable 'ffmpeg'
|
||||
$ffprobe = Find-Executable 'ffprobe'
|
||||
|
||||
if ($Force -or -not $ffmpeg -or -not $ffprobe) {
|
||||
$winget = Get-Command winget -ErrorAction SilentlyContinue
|
||||
if (-not $winget) {
|
||||
throw '未找到 winget。请先从 Microsoft Store 安装“应用安装程序”,或使用项目 Dockerfile 运行后端。'
|
||||
}
|
||||
|
||||
Write-Host '正在通过 winget 安装 FFmpeg...' -ForegroundColor Cyan
|
||||
& winget install --id Gyan.FFmpeg --exact --source winget `
|
||||
--accept-package-agreements --accept-source-agreements
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "winget 安装失败,退出码: $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$ffmpeg = Find-Executable 'ffmpeg'
|
||||
$ffprobe = Find-Executable 'ffprobe'
|
||||
}
|
||||
|
||||
if (-not $ffmpeg -or -not $ffprobe) {
|
||||
throw 'FFmpeg 已安装但未能定位 ffmpeg.exe 或 ffprobe.exe。请重新打开 PowerShell 后再次运行脚本。'
|
||||
}
|
||||
|
||||
$binDirectory = Split-Path -Parent $ffmpeg
|
||||
[Environment]::SetEnvironmentVariable('FFMPEG_PATH', $ffmpeg, 'User')
|
||||
[Environment]::SetEnvironmentVariable('FFPROBE_PATH', $ffprobe, 'User')
|
||||
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$pathParts = @($userPath -split ';' | Where-Object { $_ })
|
||||
if ($pathParts -notcontains $binDirectory) {
|
||||
$newPath = (@($pathParts) + $binDirectory) -join ';'
|
||||
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
||||
}
|
||||
|
||||
$env:FFMPEG_PATH = $ffmpeg
|
||||
$env:FFPROBE_PATH = $ffprobe
|
||||
if (($env:Path -split ';') -notcontains $binDirectory) {
|
||||
$env:Path = "$binDirectory;$env:Path"
|
||||
}
|
||||
|
||||
Write-Host "ffmpeg: $ffmpeg" -ForegroundColor Green
|
||||
Write-Host "ffprobe: $ffprobe" -ForegroundColor Green
|
||||
|
||||
& $ffmpeg -hide_banner -version | Select-Object -First 1
|
||||
& $ffprobe -hide_banner -version | Select-Object -First 1
|
||||
|
||||
$encoders = & $ffmpeg -hide_banner -encoders 2>&1 | Out-String
|
||||
if ($encoders -notmatch '\blibx264\b') {
|
||||
throw '当前 FFmpeg 不包含 libx264 编码器,无法满足短剧合成配置。'
|
||||
}
|
||||
if ($encoders -notmatch '\baac\b') {
|
||||
throw '当前 FFmpeg 不包含 AAC 编码器,无法满足短剧合成配置。'
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '安装与配置完成。请完全重启 IntelliJ IDEA 和 ruoyi-ai 后端服务。' -ForegroundColor Yellow
|
||||
Write-Host '重启后,Spring 将从 FFMPEG_PATH 和 FFPROBE_PATH 读取绝对路径。'
|
||||
File diff suppressed because one or more lines are too long
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : localhost-mysql
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80045 (8.0.45)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ruoyi-ai
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80045 (8.0.45)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 19/04/2026 13:36:41
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_model
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `chat_model`;
|
||||
CREATE TABLE `chat_model` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`category` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型分类',
|
||||
`model_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型名称',
|
||||
`provider_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型供应商',
|
||||
`model_describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型描述',
|
||||
`model_dimension` int NULL DEFAULT NULL COMMENT '模型维度',
|
||||
`model_show` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '是否显示',
|
||||
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求地址',
|
||||
`api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密钥',
|
||||
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
|
||||
`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '模型管理' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of chat_model
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型,可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言,并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中,BGE-M3展现出显著优势,其性能在MIRACL、MKQA等国际基准测试中位居前列。此外,针对长文档检索场景,该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_provider
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `chat_provider`;
|
||||
CREATE TABLE `chat_provider` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`provider_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商名称',
|
||||
`provider_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商编码',
|
||||
`provider_icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商图标',
|
||||
`provider_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商描述',
|
||||
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'API地址',
|
||||
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
|
||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`version` int NULL DEFAULT NULL COMMENT '版本',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
|
||||
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of chat_provider
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务,支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : localhost-mysql
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80045 (8.0.45)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ruoyi-ai
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80045 (8.0.45)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 20/04/2026 15:30:00
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- 新增:重排序模型(chat_model)
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_model`
|
||||
(id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, create_dept, create_by, create_time, update_by, update_time, remark, tenant_id)
|
||||
VALUES(2045071617578237953, 'rerank', 'rerank', 'zhipu', '智谱重排序', NULL, 'Y', 'https://open.bigmodel.cn', 'e9xx', 103, 1, '2026-04-17 17:27:24', 1, '2026-04-20 15:21:48', '智谱重排序', 0);
|
||||
|
||||
INSERT INTO `chat_model`
|
||||
(id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, create_dept, create_by, create_time, update_by, update_time, remark, tenant_id)
|
||||
VALUES(2046119803482902530, 'rerank', 'qwen3-rerank', 'qianwen', '千问3重排序', NULL, NULL, 'https://dashscope.aliyuncs.com', 'sk-xx', 103, 1, '2026-04-20 14:52:31', 1, '2026-04-20 15:03:13', '千问3文本重排序', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- 新增:字典类型 - 重排序模型分类
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_dict_data`
|
||||
(dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES(2045070879435259905, '000000', 4, '重排序', 'rerank', 'chat_model_category', NULL, '#000000', 'N', 103, 1, '2026-04-17 17:24:28', 1, '2026-04-19 01:02:20', '重排序模型');
|
||||
|
||||
-- ----------------------------
|
||||
-- 修改表:knowledge_info 增加重排序相关字段
|
||||
-- ----------------------------
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `enable_rerank` tinyint DEFAULT 0 NULL COMMENT '是否启用重排序(0否 1是)';
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_score_threshold` double NULL COMMENT '重排序相关性分数阈值';
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_top_n` int NULL COMMENT '重排序后返回的文档数量';
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_model` varchar(100) NULL COMMENT '重排序模型名称';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
@@ -1,14 +0,0 @@
|
||||
-- 为知识库信息表新增检索配置字段 (剔除了已存在的重排字段)
|
||||
ALTER TABLE knowledge_info
|
||||
ADD COLUMN similarity_threshold DOUBLE DEFAULT 0.5 COMMENT '相似度阈值'
|
||||
AFTER retrieve_limit;
|
||||
|
||||
ALTER TABLE knowledge_info ADD COLUMN enable_hybrid tinyint(1) DEFAULT 0 COMMENT '是否启用混合检索';
|
||||
ALTER TABLE knowledge_info ADD COLUMN hybrid_alpha double DEFAULT 0.5 COMMENT '混合检索权重比例 (0.0=纯向量, 1.0=纯关键词)';
|
||||
|
||||
-- 为知识片段表增加全文索引及关联ID
|
||||
ALTER TABLE knowledge_fragment ADD COLUMN knowledge_id bigint COMMENT '知识库ID';
|
||||
ALTER TABLE knowledge_fragment ADD FULLTEXT INDEX ft_content (content) WITH PARSER ngram;
|
||||
|
||||
-- 为知识库附件表增加解析状态字段
|
||||
ALTER TABLE `knowledge_attach` ADD COLUMN `status` TINYINT DEFAULT 0 COMMENT '解析状态: 0待解析, 1解析中, 2已解析, 3解析失败';
|
||||
16
pom.xml
16
pom.xml
@@ -13,7 +13,7 @@
|
||||
<description>全栈式AI开发平台</description>
|
||||
|
||||
<properties>
|
||||
<revision>3.0.0</revision>
|
||||
<revision>3.1.0</revision>
|
||||
<spring-boot.version>3.5.8</spring-boot.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@@ -54,17 +54,21 @@
|
||||
<!-- Jackson XML -->
|
||||
<jackson-dataformat-xml.version>2.18.2</jackson-dataformat-xml.version>
|
||||
<!-- AI 相关依赖 -->
|
||||
<langchain4j.version>1.13.0</langchain4j.version>
|
||||
<langchain4j.community.version>1.13.0-beta23</langchain4j.community.version>
|
||||
<langgraph4j.version>1.5.3</langgraph4j.version>
|
||||
|
||||
<langchain4j.version>1.17.2</langchain4j.version>
|
||||
<langchain4j.beta.version>1.17.2-beta27</langchain4j.beta.version>
|
||||
<langchain4j.community.version>1.17.0-beta27</langchain4j.community.version>
|
||||
<langgraph4j.version>1.8.20</langgraph4j.version>
|
||||
|
||||
<weaviate.version>1.19.6</weaviate.version>
|
||||
<dify.version>1.0.7</dify.version>
|
||||
<dify.version>1.2.7</dify.version>
|
||||
<coze.version>0.4.2</coze.version>
|
||||
|
||||
<!-- gRPC 版本 - 解决 Milvus SDK 依赖冲突 -->
|
||||
<grpc.version>1.62.2</grpc.version>
|
||||
<!-- Apache Commons Compress - 用于POI处理ZIP格式 -->
|
||||
<commons-compress.version>1.27.1</commons-compress.version>
|
||||
|
||||
|
||||
<avatar-generator.version>1.1.0</avatar-generator.version>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<knife4j.version>4.4.0</knife4j.version>
|
||||
|
||||
@@ -1,31 +1,59 @@
|
||||
# 贝尔实验室 Spring 官方推荐镜像 JDK下载地址 https://bell-sw.com/pages/downloads/
|
||||
# FFmpeg-enabled variant of ruoyi-admin/Dockerfile.
|
||||
FROM bellsoft/liberica-openjdk-rocky:17.0.16-cds
|
||||
#FROM bellsoft/liberica-openjdk-rocky:21.0.8-cds
|
||||
#FROM findepi/graalvm:java17-native
|
||||
|
||||
LABEL maintainer="Lion Li"
|
||||
|
||||
# Rocky base repositories do not provide the full codec build. RPM Fusion Free
|
||||
# supplies ffmpeg with libx264, while the checks below prevent a reduced build
|
||||
# from reaching production unnoticed.
|
||||
RUN set -eux; \
|
||||
dnf -y install dnf-plugins-core epel-release; \
|
||||
. /etc/os-release; \
|
||||
rocky_major="${VERSION_ID%%.*}"; \
|
||||
if [ "${rocky_major}" -ge 9 ]; then \
|
||||
dnf config-manager --set-enabled crb; \
|
||||
else \
|
||||
dnf config-manager --set-enabled powertools; \
|
||||
fi; \
|
||||
dnf -y install "https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-${rocky_major}.noarch.rpm"; \
|
||||
dnf -y install ffmpeg; \
|
||||
for filter in scale pad fps setsar format tpad trim settb setpts xfade \
|
||||
aresample aformat apad atrim asetpts anullsrc concat acrossfade; do \
|
||||
ffmpeg -hide_banner -filters 2>/dev/null | grep -Eq "[[:space:]]${filter}[[:space:]]"; \
|
||||
done; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q dissolve; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q fadeblack; \
|
||||
ffmpeg -hide_banner -h filter=xfade 2>&1 | grep -q slideleft; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]libx264[[:space:]]'; \
|
||||
ffmpeg -hide_banner -encoders 2>/dev/null | grep -Eq '[[:space:]]aac[[:space:]]'; \
|
||||
ffprobe -hide_banner -version >/dev/null; \
|
||||
dnf clean all; \
|
||||
rm -rf /var/cache/dnf
|
||||
|
||||
RUN mkdir -p /ruoyi/server/logs \
|
||||
/ruoyi/server/temp \
|
||||
/ruoyi/skywalking/agent
|
||||
|
||||
WORKDIR /ruoyi/server
|
||||
|
||||
ENV SERVER_PORT=8080 SNAIL_PORT=28080 LANG=C.UTF-8 LC_ALL=C.UTF-8 JAVA_OPTS=""
|
||||
ENV SERVER_PORT=8080 \
|
||||
SNAIL_PORT=28080 \
|
||||
LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
JAVA_OPTS="" \
|
||||
FFMPEG_PATH=/usr/bin/ffmpeg \
|
||||
FFPROBE_PATH=/usr/bin/ffprobe
|
||||
|
||||
EXPOSE ${SERVER_PORT}
|
||||
# 暴露 snail job 客户端端口 用于定时任务调度中心通信
|
||||
EXPOSE ${SNAIL_PORT}
|
||||
|
||||
ADD ./target/ruoyi-admin.jar ./app.jar
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -Dserver.port=${SERVER_PORT} \
|
||||
ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom \
|
||||
-Djava.io.tmpdir=/ruoyi/server/temp \
|
||||
-Dserver.port=${SERVER_PORT} \
|
||||
-Dsnail-job.port=${SNAIL_PORT} \
|
||||
# 应用名称 如果想区分集群节点监控 改成不同的名称即可
|
||||
#-Dskywalking.agent.service_name=ruoyi-server \
|
||||
#-javaagent:/ruoyi/skywalking/agent/skywalking-agent.jar \
|
||||
-XX:+HeapDumpOnOutOfMemoryError -XX:+UseZGC ${JAVA_OPTS} \
|
||||
-jar app.jar
|
||||
|
||||
|
||||
@@ -313,3 +313,37 @@ vector-store:
|
||||
collectionname: LocalKnowledge
|
||||
api-key:
|
||||
use-tls: false
|
||||
|
||||
# 短剧成片合成
|
||||
short-drama:
|
||||
composition:
|
||||
ffmpeg-path: ${FFMPEG_PATH:ffmpeg}
|
||||
ffprobe-path: ${FFPROBE_PATH:ffprobe}
|
||||
fps: ${SHORT_DRAMA_COMPOSITION_FPS:30}
|
||||
audio-sample-rate: ${SHORT_DRAMA_COMPOSITION_AUDIO_SAMPLE_RATE:48000}
|
||||
video-codec: ${SHORT_DRAMA_COMPOSITION_VIDEO_CODEC:libx264}
|
||||
audio-codec: ${SHORT_DRAMA_COMPOSITION_AUDIO_CODEC:aac}
|
||||
preset: ${SHORT_DRAMA_COMPOSITION_PRESET:medium}
|
||||
crf: ${SHORT_DRAMA_COMPOSITION_CRF:20}
|
||||
audio-bitrate: ${SHORT_DRAMA_COMPOSITION_AUDIO_BITRATE:192k}
|
||||
max-clips: ${SHORT_DRAMA_COMPOSITION_MAX_CLIPS:100}
|
||||
max-transition-seconds: ${SHORT_DRAMA_COMPOSITION_MAX_TRANSITION_SECONDS:2.0}
|
||||
minimum-output-bytes: ${SHORT_DRAMA_COMPOSITION_MINIMUM_OUTPUT_BYTES:1024}
|
||||
max-process-output-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_PROCESS_OUTPUT_BYTES:1048576}
|
||||
max-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_SOURCE_BYTES:536870912}
|
||||
max-total-source-bytes: ${SHORT_DRAMA_COMPOSITION_MAX_TOTAL_SOURCE_BYTES:2147483648}
|
||||
probe-timeout: ${SHORT_DRAMA_COMPOSITION_PROBE_TIMEOUT:30s}
|
||||
process-timeout: ${SHORT_DRAMA_COMPOSITION_PROCESS_TIMEOUT:30m}
|
||||
job-stale-after: ${SHORT_DRAMA_COMPOSITION_JOB_STALE_AFTER:45m}
|
||||
worker-core-size: ${SHORT_DRAMA_COMPOSITION_WORKER_CORE_SIZE:1}
|
||||
worker-max-size: ${SHORT_DRAMA_COMPOSITION_WORKER_MAX_SIZE:2}
|
||||
worker-queue-capacity: ${SHORT_DRAMA_COMPOSITION_WORKER_QUEUE_CAPACITY:8}
|
||||
storage-mode: ${SHORT_DRAMA_COMPOSITION_STORAGE_MODE:local}
|
||||
local-output-directory: ${SHORT_DRAMA_COMPOSITION_LOCAL_OUTPUT_DIRECTORY:logs/short-drama-compositions}
|
||||
download:
|
||||
connect-timeout: ${SHORT_DRAMA_DOWNLOAD_CONNECT_TIMEOUT:30s}
|
||||
call-timeout: ${SHORT_DRAMA_DOWNLOAD_CALL_TIMEOUT:10m}
|
||||
max-redirects: ${SHORT_DRAMA_DOWNLOAD_MAX_REDIRECTS:5}
|
||||
# 生产环境建议配置为视频供应商或 OSS/CDN 域名,多个值用逗号分隔。
|
||||
allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_ALLOWED_HOSTS:}
|
||||
fake-ip-allowed-hosts: ${SHORT_DRAMA_DOWNLOAD_FAKE_IP_ALLOWED_HOSTS:atlas-media.oss-us-west-1.aliyuncs.com}
|
||||
|
||||
@@ -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() + "]");
|
||||
}
|
||||
|
||||
@@ -265,10 +265,6 @@ public class WorkflowEngine {
|
||||
String node = streamingOutput.node();
|
||||
String chunk = streamingOutput.chunk();
|
||||
log.info("node:{},chunk:{}", node, chunk);
|
||||
Map<String, String> strMap = new HashMap<>();
|
||||
strMap.put("ck", chunk);
|
||||
// SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", strMap.toString());
|
||||
|
||||
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
|
||||
} else {
|
||||
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
# MCP工具管理模块 - API接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了MCP工具管理模块的REST API接口,供前端开发人员参考。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base URL**: `/api/mcp`
|
||||
- **认证方式**: Bearer Token (SaToken)
|
||||
- **响应格式**: JSON
|
||||
|
||||
---
|
||||
|
||||
## 1. MCP工具管理
|
||||
|
||||
### 1.1 查询工具列表(分页)
|
||||
|
||||
**接口**: `GET /tool/list`
|
||||
|
||||
**权限**: `mcp:tool:list`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| name | String | 否 | 工具名称(模糊查询) |
|
||||
| description | String | 否 | 工具描述(模糊查询) |
|
||||
| type | String | 否 | 工具类型:LOCAL/REMOTE/BUILTIN |
|
||||
| status | String | 否 | 状态:0-启用, 1-禁用 |
|
||||
| pageNum | Integer | 是 | 页码,默认1 |
|
||||
| pageSize | Integer | 是 | 每页数量,默认10 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ReadFileTool",
|
||||
"description": "读取文件内容工具",
|
||||
"type": "BUILTIN",
|
||||
"status": "0",
|
||||
"configJson": null,
|
||||
"createTime": "2026-03-08 10:00:00",
|
||||
"updateTime": "2026-03-08 10:00:00"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 查询工具列表(不分页)
|
||||
|
||||
**接口**: `GET /tool/all`
|
||||
|
||||
**权限**: `mcp:tool:list`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| keyword | String | 否 | 关键词 |
|
||||
| type | String | 否 | 工具类型 |
|
||||
| status | String | 否 | 状态 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ReadFileTool",
|
||||
"description": "读取文件内容工具",
|
||||
"type": "BUILTIN",
|
||||
"status": "0"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 获取工具详情
|
||||
|
||||
**接口**: `GET /tool/{id}`
|
||||
|
||||
**权限**: `mcp:tool:query`
|
||||
|
||||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 工具ID |
|
||||
|
||||
### 1.4 新增工具
|
||||
|
||||
**接口**: `POST /tool`
|
||||
|
||||
**权限**: `mcp:tool:add`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "MyMcpTool",
|
||||
"description": "我的MCP工具",
|
||||
"type": "REMOTE",
|
||||
"status": "0",
|
||||
"configJson": "{\"baseUrl\": \"http://localhost:8080/mcp\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 修改工具
|
||||
|
||||
**接口**: `PUT /tool`
|
||||
|
||||
**权限**: `mcp:tool:edit`
|
||||
|
||||
**请求体**: 同新增工具
|
||||
|
||||
### 1.6 删除工具
|
||||
|
||||
**接口**: `DELETE /tool/{ids}`
|
||||
|
||||
**权限**: `mcp:tool:remove`
|
||||
|
||||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| ids | String | 是 | 工具ID,多个用逗号分隔 |
|
||||
|
||||
### 1.7 更新工具状态
|
||||
|
||||
**接口**: `PUT /tool/{id}/status`
|
||||
|
||||
**权限**: `mcp:tool:edit`
|
||||
|
||||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 工具ID |
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| status | String | 是 | 状态:0-启用, 1-禁用 |
|
||||
|
||||
### 1.8 测试工具连接
|
||||
|
||||
**接口**: `POST /tool/{id}/test`
|
||||
|
||||
**权限**: `mcp:tool:query`
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "连接测试成功",
|
||||
"toolCount": 5,
|
||||
"tools": ["tool1", "tool2", "tool3", "tool4", "tool5"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. MCP市场管理
|
||||
|
||||
### 2.1 查询市场列表
|
||||
|
||||
**接口**: `GET /market/list`
|
||||
|
||||
**权限**: `mcp:market:list`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| name | String | 否 | 市场名称 |
|
||||
| description | String | 否 | 市场描述 |
|
||||
| status | String | 否 | 状态 |
|
||||
| pageNum | Integer | 是 | 页码 |
|
||||
| pageSize | Integer | 是 | 每页数量 |
|
||||
|
||||
### 2.2 获取市场工具列表
|
||||
|
||||
**接口**: `GET /market/{marketId}/tools`
|
||||
|
||||
**权限**: `mcp:market:query`
|
||||
|
||||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| marketId | Long | 是 | 市场ID |
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| page | Integer | 否 | 页码,默认1 |
|
||||
| size | Integer | 否 | 每页数量,默认10 |
|
||||
|
||||
### 2.3 刷新市场工具
|
||||
|
||||
**接口**: `POST /market/{marketId}/refresh`
|
||||
|
||||
**权限**: `mcp:market:edit`
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "刷新成功",
|
||||
"addedCount": 3,
|
||||
"updatedCount": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 加载工具到本地
|
||||
|
||||
**接口**: `POST /market/tool/{toolId}/load`
|
||||
|
||||
**权限**: `mcp:market:edit`
|
||||
|
||||
**路径参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| toolId | Long | 是 | 市场工具ID |
|
||||
|
||||
### 2.5 批量加载工具
|
||||
|
||||
**接口**: `POST /market/tools/batchLoad`
|
||||
|
||||
**权限**: `mcp:market:edit`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"toolIds": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具调用日志
|
||||
|
||||
### 3.1 查询调用日志
|
||||
|
||||
**接口**: `GET /tool/callLog`
|
||||
|
||||
**权限**: `mcp:tool:query`
|
||||
|
||||
**请求参数**:
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| toolId | Long | 否 | 工具ID |
|
||||
| sessionId | Long | 否 | 会话ID |
|
||||
| startDate | Date | 否 | 开始日期 |
|
||||
| endDate | Date | 否 | 结束日期 |
|
||||
| pageNum | Integer | 是 | 页码 |
|
||||
| pageSize | Integer | 是 | 每页数量 |
|
||||
|
||||
### 3.2 获取工具统计
|
||||
|
||||
**接口**: `GET /tool/{toolId}/metrics`
|
||||
|
||||
**权限**: `mcp:tool:query`
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"toolId": 1,
|
||||
"toolName": "ReadFileTool",
|
||||
"today": {
|
||||
"callCount": 100,
|
||||
"successCount": 95,
|
||||
"failureCount": 5,
|
||||
"avgDurationMs": 150,
|
||||
"successRate": 95.0
|
||||
},
|
||||
"week": {
|
||||
"callCount": 500,
|
||||
"successCount": 475,
|
||||
"failureCount": 25,
|
||||
"avgDurationMs": 160,
|
||||
"successRate": 95.0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 状态码说明
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 请求成功 |
|
||||
| 401 | 未认证 |
|
||||
| 403 | 无权限 |
|
||||
| 404 | 资源不存在 |
|
||||
| 500 | 服务器错误 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端页面需求
|
||||
|
||||
### 5.1 MCP工具管理页面 (`/mcp/tool`)
|
||||
|
||||
**功能**:
|
||||
- 工具列表展示(分页)
|
||||
- 工具搜索和筛选
|
||||
- 新增/编辑/删除工具
|
||||
- 工具状态切换
|
||||
- 工具连接测试
|
||||
|
||||
**表格列**:
|
||||
- 工具名称
|
||||
- 工具描述
|
||||
- 工具类型(标签显示)
|
||||
- 状态(开关)
|
||||
- 创建时间
|
||||
- 操作(编辑、删除、测试)
|
||||
|
||||
### 5.2 MCP市场管理页面 (`/mcp/market`)
|
||||
|
||||
**功能**:
|
||||
- 市场列表展示
|
||||
- 市场工具浏览
|
||||
- 刷新市场工具
|
||||
- 加载工具到本地
|
||||
|
||||
### 5.3 工具调用日志页面 (`/mcp/log`)
|
||||
|
||||
**功能**:
|
||||
- 调用日志列表
|
||||
- 按工具/日期筛选
|
||||
- 成功率统计
|
||||
- 响应时间统计
|
||||
|
||||
**图表**:
|
||||
- 每日调用次数趋势图
|
||||
- 工具调用成功率饼图
|
||||
- 平均响应时间柱状图
|
||||
@@ -1,152 +0,0 @@
|
||||
# 数据库操作智能体实现总结
|
||||
|
||||
## 概述
|
||||
基于 LangChain4j 的 **Pure agentic AI** 模式,完成了一个智能数据库查询系统。该系统能够根据用户的自然语言问题,自动分析数据库结构、生成查询计划并执行相应的数据库操作。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 1. 整体框架
|
||||
```
|
||||
用户请求 → SupervisorAgent(协调器) → SqlAgent(数据库专家) → 数据库工具 → 数据库
|
||||
↓
|
||||
结果处理与响应
|
||||
```
|
||||
|
||||
### 2. 核心组件
|
||||
|
||||
#### A. SqlAgent (数据库查询专家)
|
||||
- **文件**: `org.ruoyi.agent.SqlAgent`
|
||||
- **职责**: 根据用户的自然语言问题,调用相应的工具查询数据库
|
||||
- **使用的工具**:
|
||||
- `QueryAllTablesTool`: 查询所有表名和注释
|
||||
- `QueryTableSchemaTool`: 查询表的DDL(CREATE TABLE语句)
|
||||
- `ExecuteSqlQueryTool`: 执行SELECT查询
|
||||
|
||||
```java
|
||||
public interface SqlAgent {
|
||||
@SystemMessage("...") // 详细的系统提示
|
||||
@UserMessage("请回答以下问题:{{query}}")
|
||||
@Agent("一个智能数据库查询助手...")
|
||||
String getData(@V("query") String query);
|
||||
}
|
||||
```
|
||||
|
||||
#### B. SupervisorAgent (总体协调器)
|
||||
- 在 `OpenAIServiceImpl.doAgent()` 中创建
|
||||
- 作用:协调 SqlAgent 的执行,管理任务流程
|
||||
- 响应策略:`SUMMARY` - 返回所有操作的摘要
|
||||
|
||||
```java
|
||||
SupervisorAgent supervisor = AgenticServices
|
||||
.supervisorBuilder()
|
||||
.chatModel(PLANNER_MODEL)
|
||||
.subAgents(sqlAgent)
|
||||
.responseStrategy(SupervisorResponseStrategy.SUMMARY)
|
||||
.build();
|
||||
```
|
||||
|
||||
#### C. 数据库工具 (Tools)
|
||||
|
||||
##### 1. QueryAllTablesTool
|
||||
```java
|
||||
@Tool("Query all tables in the database and return table names and basic information")
|
||||
public String queryAllTables()
|
||||
```
|
||||
- 返回数据库中所有表的名称和注释
|
||||
- 使用注入的 `agentDataSource` DataSource
|
||||
|
||||
##### 2. QueryTableSchemaTool
|
||||
```java
|
||||
@Tool("Query the CREATE TABLE statement (DDL) for a specific table by table name")
|
||||
public String queryTableSchema(String tableName)
|
||||
```
|
||||
- 返回指定表的建表SQL语句
|
||||
- 包含SQL注入防护(表名有效性验证)
|
||||
|
||||
##### 3. ExecuteSqlQueryTool
|
||||
```java
|
||||
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
|
||||
public String executeSql(String sql)
|
||||
```
|
||||
- 执行SELECT查询(安全性考虑,不允许执行其他操作)
|
||||
- 格式化查询结果,最多显示前20行
|
||||
|
||||
### 3. 配置体系
|
||||
|
||||
#### AgentMysqlProperties
|
||||
配置文件前缀:`agent.mysql`
|
||||
```yaml
|
||||
agent:
|
||||
mysql:
|
||||
enabled: true
|
||||
url: jdbc:mysql://localhost:3306/your_database
|
||||
username: your_username
|
||||
password: your_password
|
||||
max-pool-size: 10
|
||||
min-idle: 2
|
||||
```
|
||||
|
||||
#### AgentMysqlConfig
|
||||
- 创建独立的 DataSource Bean (`agentDataSource`)
|
||||
- 使用 HikariCP 连接池管理
|
||||
- 与项目主数据源隔离
|
||||
|
||||
#### TableSchemaManager
|
||||
- 在应用启动时初始化表结构缓存
|
||||
- 使用 `ConcurrentHashMap` 存储结构信息
|
||||
- 支持按需刷新单个表的结构
|
||||
|
||||
## 工作流程示例
|
||||
|
||||
### 用户查询: "数据库有哪些表?"
|
||||
|
||||
```
|
||||
1. SupervisorAgent 接收请求
|
||||
↓
|
||||
2. SupervisorAgent 分析请求,决定调用 SqlAgent
|
||||
↓
|
||||
3. SqlAgent 理解需求,调用 QueryAllTablesTool
|
||||
↓
|
||||
4. QueryAllTablesTool 连接数据库,获取所有表
|
||||
↓
|
||||
5. 结果返回给 SqlAgent
|
||||
↓
|
||||
6. SqlAgent 格式化结果
|
||||
↓
|
||||
7. SupervisorAgent 生成最终摘要
|
||||
↓
|
||||
8. 结果通过流式处理器返回给用户
|
||||
```
|
||||
|
||||
### 用户查询: "查询 sys_user 表中有多少条记录"
|
||||
|
||||
```
|
||||
1. SqlAgent 接收请求
|
||||
↓
|
||||
2. SqlAgent 分析需求,可能先调用 QueryTableSchemaTool 了解表结构
|
||||
↓
|
||||
3. 然后调用 ExecuteSqlQueryTool 执行 "SELECT COUNT(*) FROM sys_user"
|
||||
↓
|
||||
4. 获取查询结果并返回
|
||||
```
|
||||
|
||||
## Agentic AI 特性
|
||||
|
||||
### 自适应决策
|
||||
- Agent 能根据上下文和之前的结果决定下一步操作
|
||||
- 不是预定义的固定流程,而是动态适应
|
||||
|
||||
### 例子
|
||||
当询问"查询部门表的字段信息时":
|
||||
- SqlAgent 可能先调用 `QueryTableSchemaTool` 获取建表SQL
|
||||
- 如果发现需要具体的数据示例,会继续调用 `ExecuteSqlQueryTool`
|
||||
- 整个决策过程由 LLM 驱动,非硬编码
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **数据源隔离**: Agent 使用独立的数据源(agentDataSource),与主应用隔离
|
||||
2. **SQL验证**: 只允许执行 SELECT 查询
|
||||
3. **表名验证**: 表名必须通过正则表达式验证(防止SQL注入)
|
||||
4. **权限限制**: 可通过 AGENT_ALLOWED_TABLES 环境变量限制可访问的表
|
||||
5. **凭证管理**: 数据库凭证通过配置文件管理,不硬编码
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-agentic</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -69,7 +69,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-document-parser-apache-tika</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
@@ -82,39 +82,39 @@
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-milvus</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-weaviate</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-qdrant</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-mcp</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- LangChain4j Skills - 技能模块 -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-skills</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-experimental-skills-shell</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
<version>${langchain4j.beta.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -155,10 +155,24 @@
|
||||
<version>${dify.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HikariCP 数据库连接池 -->
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<groupId>com.coze</groupId>
|
||||
<artifactId>coze-api</artifactId>
|
||||
<version>${coze.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL JDBC 驱动 -->
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaScriptResult;
|
||||
|
||||
/**
|
||||
* 短剧剧本打磨 Agent —— 使用 langchain4j AiServices 结构化输出
|
||||
* <p>
|
||||
* 框架自动生成 JSON Schema 并强制 LLM 返回符合结构的数据,无需手工 parse。
|
||||
*
|
||||
* @author ageerle
|
||||
*/
|
||||
public interface ShortDramaScriptAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
你是顶级短剧编剧和创意总监。请根据用户的一个创意想法,创作完整的短剧剧本。
|
||||
|
||||
【核心原则 - 最高优先级】
|
||||
1. 剧本必须完整、有张力、有画面感
|
||||
2. 角色要有鲜明性格,不是工具人
|
||||
3. 情节紧凑,每句台词都推动剧情
|
||||
4. 场景描写具体,让分镜师能直接画出画面
|
||||
5. 对话自然有力,避免废话
|
||||
|
||||
【剧本格式】
|
||||
使用标准剧本格式,包含以下元素:
|
||||
1. 场景头(Scene Heading):内景/外景+地点+时间,如"内景 客厅 清晨"
|
||||
2. 场景描述(Scene Description):简洁描述场景环境、布局、关键道具
|
||||
3. 动作描述(Action):描述角色的动作、表情、行为,连续段落形式
|
||||
4. 对话(Dialogue):角色名: 台词内容
|
||||
5. 画外音(Voiceover):旁白、独白、回忆中的声音
|
||||
|
||||
【剧本长度要求】
|
||||
- scriptText:1000-3000字,含完整的开场、冲突发展、高潮、结尾
|
||||
- outlineText:400-800字,概述完整故事线
|
||||
|
||||
【角色塑造要求】
|
||||
- 每个角色要有明确的性格标签(如:霸道总裁、温柔女医、腹黑谋士)
|
||||
- 角色之间要有清晰的关系和冲突
|
||||
- 对话要符合角色性格
|
||||
|
||||
【情节要求】
|
||||
- 必须有清晰的冲突和反转
|
||||
- 情绪节奏要有起伏(紧张→舒缓→爆发)
|
||||
- 结尾要有记忆点(反转/留白/情感升华)
|
||||
""")
|
||||
@UserMessage("用户期望项目名:{{projectName}}\n用户创意:{{idea}}")
|
||||
ShortDramaScriptResult polish(@V("projectName") String projectName, @V("idea") String idea);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.ruoyi.config.agent;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* 磁盘 Skills 目录路径解析器
|
||||
* <p>
|
||||
* langchain4j 的 ShellSkills 通过 FileSystemSkillLoader 从磁盘加载 SKILL.md,
|
||||
* 路径硬编码在 ChatServiceFacade 中。抽到此工具类供智能体管理端与聊天流程共用,
|
||||
* 避免两处路径漂移。
|
||||
*
|
||||
* @author ruoyi team
|
||||
*/
|
||||
public final class SkillsPathResolver {
|
||||
|
||||
private SkillsPathResolver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* skills 目录相对项目根目录的路径
|
||||
*/
|
||||
private static final String SKILLS_RELATIVE_PATH = "ruoyi-admin/src/main/resources/skills";
|
||||
|
||||
/**
|
||||
* 返回磁盘 skills 目录的绝对路径
|
||||
*/
|
||||
public static Path resolveSkillsPath() {
|
||||
String userDir = System.getProperty("user.dir");
|
||||
return Path.of(userDir, SKILLS_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.ruoyi.constant;
|
||||
|
||||
/**
|
||||
* 短剧图片资产常量 — 三视图 prompt 工程
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public final class ShortDramaImageConstants {
|
||||
|
||||
private ShortDramaImageConstants() {}
|
||||
|
||||
/**
|
||||
* 角色三视图 prompt 前缀,生图时自动拼到用户 prompt 之前。
|
||||
* 把风格和构图指令放最前面,避免被角色描述稀释导致画风漂移。
|
||||
*/
|
||||
public static final String CHARACTER_PROMPT_PREFIX =
|
||||
"character design sheet, multiple views reference sheet, " +
|
||||
"front view / side view / back view full body, " +
|
||||
"clean white background, no props, no text. ";
|
||||
|
||||
/**
|
||||
* 角色三视图 prompt 后缀,生图时自动追加到用户 prompt 之后。
|
||||
* 左侧1/3正面特写 + 右侧2/3三视图横向排列(正面全身、侧面全身、背面全身)。
|
||||
*/
|
||||
public static final String CHARACTER_PROMPT_SUFFIX =
|
||||
"。角色设定图,画面分为左右两个区域:" +
|
||||
"【左侧区域】占约1/3宽度,是角色的正面特写" +
|
||||
"(完整正脸,最具辨识度的正面形态);" +
|
||||
"【右侧区域】占约2/3宽度,是角色三视图横向排列" +
|
||||
"(从左到右依次为:正面全身、侧面全身、背面全身)," +
|
||||
"三视图高度一致。纯白色背景,无其他元素。";
|
||||
|
||||
/** 场景图 prompt 前缀 */
|
||||
public static final String LOCATION_PROMPT_PREFIX = "宽广空间全景,";
|
||||
|
||||
/** 场景图 prompt 后缀 */
|
||||
public static final String LOCATION_PROMPT_SUFFIX = ",禁止出现任何角色,纯背景板";
|
||||
|
||||
/** 每个资产最多保留的图片变体数量 */
|
||||
public static final int MAX_IMAGE_VARIANTS = 20;
|
||||
|
||||
// ==================== 视觉风格 ====================
|
||||
|
||||
/** 项目视觉风格 → 生图 prompt 后缀映射,确保同项目所有图片风格一致 */
|
||||
public static final java.util.Map<String, String> ART_STYLE_PROMPTS = java.util.Map.of(
|
||||
"american-comic", "美式漫画风格,粗线条,高饱和度色彩,强烈光影对比",
|
||||
"chinese-comic", "现代国漫动画风格,Chinese donghua 2D comic style,赛璐璐平涂上色,干净锐利的黑色线稿,平面化光影无真实景深,动漫人物比例(略放大双眼、修长身形),皮肤平滑无毛孔无写实肤质,国风服饰剪裁与材质细节清晰,色彩饱满通透,画面精致干净;禁止真人写实、摄影实拍、3D渲染、CGI、厚涂油画、写实皮肤纹理、景深虚化",
|
||||
"japanese-anime", "现代日系动漫风格,赛璐璐上色,清晰干净的线条,视觉小说CG感,高质量2D风格",
|
||||
"realistic", "真实电影级画面质感,真实现实场景,色彩饱满通透,画面干净精致,真实感"
|
||||
);
|
||||
|
||||
/** 默认视觉风格 */
|
||||
public static final String DEFAULT_ART_STYLE = "realistic";
|
||||
|
||||
/** 查找 artStyle 对应的 prompt 后缀,找不到返回空字符串 */
|
||||
public static String artStylePrompt(String artStyle) {
|
||||
if (artStyle == null) return "";
|
||||
return ART_STYLE_PROMPTS.getOrDefault(artStyle, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 由项目 id 派生一个稳定的生图随机种子。
|
||||
* 同项目内所有角色/形象共用同一颗种子,渲染基调(光影、配色、笔触)更趋一致;
|
||||
* 不同项目派生不同种子,避免跨项目撞图。返回值落在 [0, 2_000_000_000),兼容各供应商。
|
||||
*/
|
||||
public static Integer styleSeed(Long projectId) {
|
||||
if (projectId == null) return null;
|
||||
long h = projectId;
|
||||
h ^= (h >>> 32);
|
||||
return (int) Math.floorMod(h * 2654435761L, 2_000_000_000L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package org.ruoyi.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.ruoyi.common.log.annotation.Log;
|
||||
import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.ruoyi.domain.bo.agent.AgentBo;
|
||||
import org.ruoyi.domain.vo.agent.AgentVo;
|
||||
import org.ruoyi.domain.vo.agent.SkillOptionVo;
|
||||
import org.ruoyi.service.agent.IAgentService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智能体管理 Controller
|
||||
*
|
||||
* @author ruoyi team
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/agent")
|
||||
public class AgentController extends BaseController {
|
||||
|
||||
private final IAgentService agentService;
|
||||
|
||||
/**
|
||||
* 分页查询智能体列表
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<AgentVo> list(AgentBo bo, PageQuery pageQuery) {
|
||||
return agentService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询智能体列表(不分页,用于导出)
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:list")
|
||||
@GetMapping("/queryList")
|
||||
public R<List<AgentVo>> queryList(AgentBo bo) {
|
||||
return R.ok(agentService.queryList(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出智能体列表
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:export")
|
||||
@Log(title = "智能体管理", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(AgentBo bo, HttpServletResponse response) {
|
||||
List<AgentVo> list = agentService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "智能体", AgentVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取智能体详情
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<AgentVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(agentService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增智能体
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:add")
|
||||
@Log(title = "智能体管理", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody AgentBo bo) {
|
||||
return toAjax(agentService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改智能体
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:edit")
|
||||
@Log(title = "智能体管理", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody AgentBo bo) {
|
||||
return toAjax(agentService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除智能体
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:remove")
|
||||
@Log(title = "智能体管理", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(agentService.deleteByIds(List.of(ids)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户端聊天页智能体下拉选项(启用状态,不需权限校验)
|
||||
*/
|
||||
@GetMapping("/agentOptions")
|
||||
public R<List<AgentVo>> agentOptions() {
|
||||
return R.ok(agentService.queryEnabledOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出磁盘上可用的 Skills(供管理端表单勾选)
|
||||
*/
|
||||
@SaCheckPermission("agent:agent:list")
|
||||
@GetMapping("/skillOptions")
|
||||
public R<List<SkillOptionVo>> skillOptions() {
|
||||
return R.ok(agentService.listSkillOptions());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.core.utils.StringUtils;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.enums.ModelType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -54,7 +55,9 @@ public class ChatModelController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/modelList")
|
||||
public R<List<ChatModelVo>> modelList(ChatModelBo bo) {
|
||||
bo.setCategory(ModelType.CHAT.getKey());
|
||||
if (StringUtils.isBlank(bo.getCategory())) {
|
||||
bo.setCategory(ModelType.CHAT.getKey());
|
||||
}
|
||||
return R.ok(chatModelService.queryList(bo));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.ruoyi.controller.chat;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.entity.audio.AudioContext;
|
||||
import org.ruoyi.common.chat.entity.image.ImageContext;
|
||||
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
|
||||
import org.ruoyi.common.chat.entity.video.VideoContext;
|
||||
import org.ruoyi.common.chat.factory.AudioServiceFactory;
|
||||
import org.ruoyi.common.chat.factory.ImageServiceFactory;
|
||||
import org.ruoyi.common.chat.factory.VideoServiceFactory;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.domain.bo.media.ImageGenerationRequest;
|
||||
import org.ruoyi.domain.bo.media.SpeechGenerationRequest;
|
||||
import org.ruoyi.domain.bo.media.VideoGenerationRequest;
|
||||
import org.ruoyi.enums.ModelType;
|
||||
import org.ruoyi.service.media.AtlasPredictionService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/media")
|
||||
public class MediaGenerationController {
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final AudioServiceFactory audioServiceFactory;
|
||||
private final ImageServiceFactory imageServiceFactory;
|
||||
private final VideoServiceFactory videoServiceFactory;
|
||||
private final AtlasPredictionService atlasPredictionService;
|
||||
|
||||
@PostMapping("/speech")
|
||||
public R<MediaGenerationResponse> speech(@Valid @RequestBody SpeechGenerationRequest request) {
|
||||
ChatModelVo model = loadModel(request.getModel(), ModelType.AUDIO.getKey());
|
||||
MediaGenerationResponse response = audioServiceFactory.getOriginalService(model.getProviderCode())
|
||||
.generateSpeech(AudioContext.builder()
|
||||
.chatModelVo(model)
|
||||
.input(request.getInput())
|
||||
.voice(request.getVoice())
|
||||
.responseFormat(request.getResponseFormat())
|
||||
.speed(request.getSpeed())
|
||||
.instructions(request.getInstructions())
|
||||
.build());
|
||||
return R.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/image")
|
||||
public R<MediaGenerationResponse> image(@Valid @RequestBody ImageGenerationRequest request) {
|
||||
ChatModelVo model = loadModel(request.getModel(), ModelType.IMAGE.getKey());
|
||||
String result = imageServiceFactory.getOriginalService(model.getProviderCode())
|
||||
.generateImage(ImageContext.builder()
|
||||
.chatModelVo(model)
|
||||
.prompt(request.getPrompt())
|
||||
.size(request.getSize())
|
||||
.seed(request.getSeed())
|
||||
.build());
|
||||
return R.ok(toImageResponse(result));
|
||||
}
|
||||
|
||||
@PostMapping("/video")
|
||||
public R<MediaGenerationResponse> video(@Valid @RequestBody VideoGenerationRequest request) {
|
||||
ChatModelVo model = loadModel(request.getModel(), ModelType.VIDEO.getKey());
|
||||
MediaGenerationResponse response = videoServiceFactory.getOriginalService(model.getProviderCode())
|
||||
.generateVideo(VideoContext.builder()
|
||||
.chatModelVo(model)
|
||||
.prompt(request.getPrompt())
|
||||
.size(request.getSize())
|
||||
.seconds(request.getSeconds())
|
||||
.quality(request.getQuality())
|
||||
.build());
|
||||
return R.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/video")
|
||||
public R<MediaGenerationResponse> videoResult(@NotBlank(message = "模型不能为空") @RequestParam String model,
|
||||
@NotBlank(message = "videoId不能为空") @RequestParam String videoId) {
|
||||
ChatModelVo chatModelVo = loadModel(model, ModelType.VIDEO.getKey());
|
||||
MediaGenerationResponse response = videoServiceFactory.getOriginalService(chatModelVo.getProviderCode())
|
||||
.retrieveVideo(VideoContext.builder()
|
||||
.chatModelVo(chatModelVo)
|
||||
.prompt("retrieve")
|
||||
.videoId(videoId)
|
||||
.build());
|
||||
return R.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/prediction")
|
||||
public R<MediaGenerationResponse> prediction(@NotBlank(message = "模型不能为空") @RequestParam String model,
|
||||
@NotBlank(message = "predictionId不能为空") @RequestParam String predictionId) {
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(model);
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("未找到模型配置: " + model);
|
||||
}
|
||||
return R.ok(atlasPredictionService.retrieve(chatModelVo, predictionId));
|
||||
}
|
||||
|
||||
private ChatModelVo loadModel(String modelName, String category) {
|
||||
ChatModelVo model = chatModelService.selectModelByName(modelName);
|
||||
if (model == null) {
|
||||
throw new IllegalArgumentException("未找到模型配置: " + modelName);
|
||||
}
|
||||
if (!category.equals(model.getCategory())) {
|
||||
throw new IllegalArgumentException("模型分类不匹配,期望: " + category + ", 实际: " + model.getCategory());
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
private MediaGenerationResponse toImageResponse(String result) {
|
||||
if (result != null && result.startsWith("{")) {
|
||||
try {
|
||||
return atlasPredictionService.toResponse(result, "image");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("图片生成响应解析失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
if (result != null && result.startsWith("data:")) {
|
||||
String mimeType = result.substring("data:".length(), result.indexOf(";base64,"));
|
||||
String b64 = result.substring(result.indexOf(";base64,") + ";base64,".length());
|
||||
return MediaGenerationResponse.builder()
|
||||
.type("image")
|
||||
.mimeType(mimeType)
|
||||
.b64Json(b64)
|
||||
.dataUrl(result)
|
||||
.build();
|
||||
}
|
||||
return MediaGenerationResponse.builder()
|
||||
.type("image")
|
||||
.mimeType("image/png")
|
||||
.url(result)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package org.ruoyi.controller.shortdrama;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.chat.entity.media.MediaGenerationResponse;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.exception.ServiceException;
|
||||
import org.ruoyi.common.core.service.OssService;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaCharacterBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaCharacterAppearanceBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaComposeVideoBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaLocationBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaProjectBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaScriptBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaStoryboardBo;
|
||||
import org.ruoyi.domain.bo.shortdrama.ShortDramaIdeaBo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaCharacterVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaCharacterAppearanceVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaComposeVideoVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaDetailVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaLocationVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaProjectVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaScriptVo;
|
||||
import org.ruoyi.domain.vo.shortdrama.ShortDramaStoryboardVo;
|
||||
import org.ruoyi.service.shortdrama.IShortDramaService;
|
||||
import org.ruoyi.service.shortdrama.IShortDramaVideoComposeService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/short-drama")
|
||||
public class ShortDramaController {
|
||||
|
||||
private final IShortDramaService shortDramaService;
|
||||
|
||||
private final IShortDramaVideoComposeService videoComposeService;
|
||||
|
||||
private final OssService ossService;
|
||||
|
||||
// ==================== 项目 ====================
|
||||
|
||||
@GetMapping("/projects")
|
||||
public R<List<ShortDramaProjectVo>> projects() {
|
||||
return R.ok(shortDramaService.listProjects(LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{projectId}")
|
||||
public R<ShortDramaDetailVo> detail(@PathVariable Long projectId) {
|
||||
return R.ok(shortDramaService.getDetail(projectId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/create-from-idea")
|
||||
public R<ShortDramaDetailVo> createFromIdea(@Valid @RequestBody ShortDramaIdeaBo bo) {
|
||||
return R.ok(shortDramaService.createFromIdea(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
/** SSE 流式创建:逐阶段推送进度,避免用户等待焦虑 */
|
||||
@PostMapping("/create-from-idea/stream")
|
||||
public SseEmitter createFromIdeaStream(@Valid @RequestBody ShortDramaIdeaBo bo) {
|
||||
return shortDramaService.createFromIdeaStream(bo, LoginHelper.getUserId());
|
||||
}
|
||||
|
||||
@PostMapping("/project")
|
||||
public R<Long> saveProject(@Valid @RequestBody ShortDramaProjectBo bo) {
|
||||
return R.ok(shortDramaService.saveProject(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PutMapping("/project")
|
||||
public R<Long> updateProject(@Valid @RequestBody ShortDramaProjectBo bo) {
|
||||
return R.ok(shortDramaService.saveProject(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/project/{projectId}")
|
||||
public R<Void> deleteProject(@NotNull @PathVariable Long projectId) {
|
||||
shortDramaService.deleteProject(projectId, LoginHelper.getUserId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// ==================== 剧本 ====================
|
||||
|
||||
@PostMapping("/script")
|
||||
public R<ShortDramaScriptVo> saveScript(@Valid @RequestBody ShortDramaScriptBo bo) {
|
||||
return R.ok(shortDramaService.saveScript(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 分镜 ====================
|
||||
|
||||
@PostMapping("/storyboards/generate")
|
||||
public R<List<ShortDramaStoryboardVo>> generate(@NotNull @RequestParam Long projectId,
|
||||
@NotNull @RequestParam Long scriptId,
|
||||
@RequestParam(required = false) String model) {
|
||||
return R.ok(shortDramaService.generateStoryboards(projectId, scriptId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/storyboard")
|
||||
public R<ShortDramaStoryboardVo> saveStoryboard(@Valid @RequestBody ShortDramaStoryboardBo bo) {
|
||||
return R.ok(shortDramaService.saveStoryboard(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/storyboard/{storyboardId}/generate-video")
|
||||
public R<ShortDramaStoryboardVo> generateVideo(@NotNull @PathVariable Long storyboardId,
|
||||
@NotBlank @RequestParam String model) {
|
||||
return R.ok(shortDramaService.generateVideo(storyboardId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/storyboard/{storyboardId}/video-result")
|
||||
public R<ShortDramaStoryboardVo> videoResult(@NotNull @PathVariable Long storyboardId,
|
||||
@NotBlank @RequestParam String model) {
|
||||
return R.ok(shortDramaService.retrieveVideo(storyboardId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/{projectId}/generate-all-videos")
|
||||
public R<List<ShortDramaStoryboardVo>> generateAllVideos(@NotNull @PathVariable Long projectId,
|
||||
@NotBlank @RequestParam String model) {
|
||||
return R.ok(shortDramaService.generateAllVideos(projectId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/{projectId}/compose-video")
|
||||
public R<ShortDramaComposeVideoVo> composeVideo(@NotNull @PathVariable Long projectId,
|
||||
@Valid @RequestBody ShortDramaComposeVideoBo bo) {
|
||||
return R.ok(videoComposeService.composeVideo(projectId, bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{projectId}/compose-video")
|
||||
public R<ShortDramaComposeVideoVo> getComposedVideo(@NotNull @PathVariable Long projectId) {
|
||||
return R.ok(videoComposeService.getComposedVideo(projectId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{projectId}/compose-video/download")
|
||||
public void downloadComposedVideo(@NotNull @PathVariable Long projectId, HttpServletResponse response)
|
||||
throws IOException {
|
||||
ShortDramaComposeVideoVo composition = videoComposeService.getComposedVideo(projectId, LoginHelper.getUserId());
|
||||
if (composition == null || !"done".equals(composition.getStatus())) {
|
||||
throw new ServiceException("成片尚未生成完成");
|
||||
}
|
||||
if (composition.getVideoOssId() != null) {
|
||||
ossService.downloadFile(composition.getVideoOssId(), response);
|
||||
return;
|
||||
}
|
||||
Path localVideo = videoComposeService.getLocalComposedVideo(projectId, LoginHelper.getUserId());
|
||||
response.setContentType("video/mp4");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=short-drama-" + projectId + ".mp4");
|
||||
response.setContentLengthLong(Files.size(localVideo));
|
||||
Files.copy(localVideo, response.getOutputStream());
|
||||
}
|
||||
|
||||
// ==================== 阶段式流水线端点 ====================
|
||||
|
||||
/** Phase 1: 剧本打磨 */
|
||||
@PostMapping("/{projectId}/polish-script")
|
||||
public R<ShortDramaDetailVo> polishScript(@NotNull @PathVariable Long projectId) {
|
||||
return R.ok(shortDramaService.polishScript(projectId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 资产分析 ====================
|
||||
|
||||
/** Phase 2: 资产分析(角色+场景提取) */
|
||||
@PostMapping("/{projectId}/analyze-assets")
|
||||
public R<ShortDramaDetailVo> analyzeAssets(@NotNull @PathVariable Long projectId,
|
||||
@NotNull @RequestParam Long scriptId) {
|
||||
return R.ok(shortDramaService.analyzeAssets(projectId, scriptId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 分镜流水线 ====================
|
||||
|
||||
/** Phase 3-6: 分镜规划+摄影规则+表演指导+分镜细化 */
|
||||
@PostMapping("/{projectId}/plan-storyboard")
|
||||
public R<List<ShortDramaStoryboardVo>> planStoryboard(@NotNull @PathVariable Long projectId,
|
||||
@NotNull @RequestParam Long scriptId,
|
||||
@RequestParam(required = false) String model) {
|
||||
return R.ok(shortDramaService.planStoryboard(projectId, scriptId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
/** Phase 3-6: SSE 流式生成分镜,持续推送规划和细化进度 */
|
||||
@PostMapping("/{projectId}/plan-storyboard/stream")
|
||||
public SseEmitter planStoryboardStream(@NotNull @PathVariable Long projectId,
|
||||
@NotNull @RequestParam Long scriptId,
|
||||
@RequestParam(required = false) String model) {
|
||||
return shortDramaService.planStoryboardStream(projectId, scriptId, model, LoginHelper.getUserId());
|
||||
}
|
||||
|
||||
/** Phase 4: 重新生成摄影规则 */
|
||||
@PostMapping("/{projectId}/photography-rules")
|
||||
public R<List<ShortDramaStoryboardVo>> generatePhotographyRules(@NotNull @PathVariable Long projectId,
|
||||
@NotNull @RequestParam Long scriptId) {
|
||||
return R.ok(shortDramaService.generatePhotographyRules(projectId, scriptId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
/** Phase 5: 重新生成表演指导 */
|
||||
@PostMapping("/{projectId}/acting-directions")
|
||||
public R<List<ShortDramaStoryboardVo>> generateActingDirections(@NotNull @PathVariable Long projectId,
|
||||
@NotNull @RequestParam Long scriptId) {
|
||||
return R.ok(shortDramaService.generateActingDirections(projectId, scriptId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 角色管理 ====================
|
||||
|
||||
@PostMapping("/character")
|
||||
public R<ShortDramaCharacterVo> saveCharacter(@Valid @RequestBody ShortDramaCharacterBo bo) {
|
||||
return R.ok(shortDramaService.saveCharacter(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PutMapping("/character")
|
||||
public R<ShortDramaCharacterVo> updateCharacter(@Valid @RequestBody ShortDramaCharacterBo bo) {
|
||||
return R.ok(shortDramaService.saveCharacter(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/character/{characterId}")
|
||||
public R<Void> deleteCharacter(@NotNull @PathVariable Long characterId) {
|
||||
shortDramaService.deleteCharacter(characterId, LoginHelper.getUserId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/character/{characterId}/generate-image")
|
||||
public R<ShortDramaCharacterVo> generateCharacterImage(@NotNull @PathVariable Long characterId,
|
||||
@NotBlank @RequestParam String model,
|
||||
@RequestParam(required = false) String referenceImageUrl) {
|
||||
return R.ok(shortDramaService.generateCharacterImage(characterId, model, referenceImageUrl, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 角色形象管理 ====================
|
||||
|
||||
@PostMapping("/character-appearance")
|
||||
public R<ShortDramaCharacterAppearanceVo> saveAppearance(@Valid @RequestBody ShortDramaCharacterAppearanceBo bo) {
|
||||
return R.ok(shortDramaService.saveAppearance(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PutMapping("/character-appearance")
|
||||
public R<ShortDramaCharacterAppearanceVo> updateAppearance(@Valid @RequestBody ShortDramaCharacterAppearanceBo bo) {
|
||||
return R.ok(shortDramaService.saveAppearance(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/character-appearance/{appearanceId}")
|
||||
public R<Void> deleteAppearance(@NotNull @PathVariable Long appearanceId) {
|
||||
shortDramaService.deleteAppearance(appearanceId, LoginHelper.getUserId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/character-appearance/{appearanceId}/generate-image")
|
||||
public R<ShortDramaCharacterAppearanceVo> generateAppearanceImage(@NotNull @PathVariable Long appearanceId,
|
||||
@NotBlank @RequestParam String model,
|
||||
@RequestParam(required = false) String referenceImageUrl) {
|
||||
return R.ok(shortDramaService.generateAppearanceImage(appearanceId, model, referenceImageUrl, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/character-appearance/{appearanceId}/regenerate")
|
||||
public R<ShortDramaCharacterAppearanceVo> regenerateAppearanceImage(@NotNull @PathVariable Long appearanceId,
|
||||
@NotBlank @RequestParam String model,
|
||||
@RequestParam(required = false) String referenceImageUrl) {
|
||||
return R.ok(shortDramaService.regenerateAppearanceImage(appearanceId, model, referenceImageUrl, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/character-appearance/{appearanceId}/select-image")
|
||||
public R<ShortDramaCharacterAppearanceVo> selectAppearanceImage(@NotNull @PathVariable Long appearanceId,
|
||||
@NotNull @RequestParam Integer index) {
|
||||
return R.ok(shortDramaService.selectAppearanceImage(appearanceId, index, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/character-appearance/{appearanceId}/image")
|
||||
public R<ShortDramaCharacterAppearanceVo> deleteAppearanceImage(@NotNull @PathVariable Long appearanceId,
|
||||
@NotNull @RequestParam Integer index) {
|
||||
return R.ok(shortDramaService.deleteAppearanceImage(appearanceId, index, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/character-appearance/{appearanceId}/undo-image")
|
||||
public R<ShortDramaCharacterAppearanceVo> undoAppearanceImage(@NotNull @PathVariable Long appearanceId) {
|
||||
return R.ok(shortDramaService.undoAppearanceImage(appearanceId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 场景管理 ====================
|
||||
|
||||
@PostMapping("/location")
|
||||
public R<ShortDramaLocationVo> saveLocation(@Valid @RequestBody ShortDramaLocationBo bo) {
|
||||
return R.ok(shortDramaService.saveLocation(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PutMapping("/location")
|
||||
public R<ShortDramaLocationVo> updateLocation(@Valid @RequestBody ShortDramaLocationBo bo) {
|
||||
return R.ok(shortDramaService.saveLocation(bo, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/location/{locationId}")
|
||||
public R<Void> deleteLocation(@NotNull @PathVariable Long locationId) {
|
||||
shortDramaService.deleteLocation(locationId, LoginHelper.getUserId());
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/location/{locationId}/generate-image")
|
||||
public R<ShortDramaLocationVo> generateLocationImage(@NotNull @PathVariable Long locationId,
|
||||
@NotBlank @RequestParam String model,
|
||||
@RequestParam(required = false) String referenceImageUrl) {
|
||||
return R.ok(shortDramaService.generateLocationImage(locationId, model, referenceImageUrl, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/location/{locationId}/regenerate")
|
||||
public R<ShortDramaLocationVo> regenerateLocationImage(@NotNull @PathVariable Long locationId,
|
||||
@NotBlank @RequestParam String model,
|
||||
@RequestParam(required = false) String referenceImageUrl) {
|
||||
return R.ok(shortDramaService.regenerateLocationImage(locationId, model, referenceImageUrl, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/location/{locationId}/select-image")
|
||||
public R<ShortDramaLocationVo> selectLocationImage(@NotNull @PathVariable Long locationId,
|
||||
@NotNull @RequestParam Integer index) {
|
||||
return R.ok(shortDramaService.selectLocationImage(locationId, index, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/location/{locationId}/image")
|
||||
public R<ShortDramaLocationVo> deleteLocationImage(@NotNull @PathVariable Long locationId,
|
||||
@NotNull @RequestParam Integer index) {
|
||||
return R.ok(shortDramaService.deleteLocationImage(locationId, index, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
@PostMapping("/location/{locationId}/undo-image")
|
||||
public R<ShortDramaLocationVo> undoLocationImage(@NotNull @PathVariable Long locationId) {
|
||||
return R.ok(shortDramaService.undoLocationImage(locationId, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
// ==================== 异步图片生成(轮询进度) ====================
|
||||
|
||||
/** 上传本地照片到图片供应商,返回当前生成会话使用的临时 URL。 */
|
||||
@PostMapping(value = "/image/upload-reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<String> uploadReferenceImage(@RequestPart("file") MultipartFile file,
|
||||
@NotBlank @RequestParam String model) {
|
||||
String temporaryUrl = shortDramaService.uploadReferenceImage(file, model, LoginHelper.getUserId());
|
||||
return R.ok("上传成功", temporaryUrl);
|
||||
}
|
||||
|
||||
/** 异步启动图片生成,返回 predictionId 供前端轮询 */
|
||||
@PostMapping("/image/start")
|
||||
public R<MediaGenerationResponse> startImage(@NotBlank @RequestParam String assetType,
|
||||
@NotNull @RequestParam Long assetId,
|
||||
@NotBlank @RequestParam String model,
|
||||
@RequestParam(required = false) String referenceImageUrl) {
|
||||
return R.ok(shortDramaService.startImageGeneration(assetType, assetId, model, referenceImageUrl, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
/** 轮询确认形象图片并保存 */
|
||||
@PostMapping("/character-appearance/{id}/confirm-image")
|
||||
public R<ShortDramaCharacterAppearanceVo> confirmAppearanceImage(@NotNull @PathVariable Long id,
|
||||
@NotBlank @RequestParam String predictionId,
|
||||
@NotBlank @RequestParam String model) {
|
||||
return R.ok(shortDramaService.confirmAppearanceImage(id, predictionId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
|
||||
/** 轮询确认场景图片并保存 */
|
||||
@PostMapping("/location/{id}/confirm-image")
|
||||
public R<ShortDramaLocationVo> confirmLocationImage(@NotNull @PathVariable Long id,
|
||||
@NotBlank @RequestParam String predictionId,
|
||||
@NotBlank @RequestParam String model) {
|
||||
return R.ok(shortDramaService.confirmLocationImage(id, predictionId, model, LoginHelper.getUserId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.ruoyi.domain.bo.agent;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import io.github.linpeilie.annotations.AutoMapping;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.domain.entity.agent.Agent;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智能体业务对象
|
||||
*
|
||||
* @author ruoyi team
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = Agent.class, reverseConvertGenerate = false)
|
||||
public class AgentBo extends BaseEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 智能体ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 智能体名称
|
||||
*/
|
||||
@NotBlank(message = "智能体名称不能为空")
|
||||
@Size(min = 0, max = 200, message = "智能体名称不能超过{max}个字符")
|
||||
private String agentName;
|
||||
|
||||
/**
|
||||
* 智能体描述
|
||||
*/
|
||||
private String agentDescribe;
|
||||
|
||||
/**
|
||||
* 展示图标/头像URL
|
||||
*/
|
||||
private String agentShow;
|
||||
|
||||
/**
|
||||
* 绑定的聊天模型ID
|
||||
*/
|
||||
@NotNull(message = "绑定模型不能为空")
|
||||
private Long modelId;
|
||||
|
||||
/**
|
||||
* 是否启用深度思考:0 否 1 是
|
||||
*/
|
||||
private String enableThinking;
|
||||
|
||||
/**
|
||||
* 自定义系统提示词
|
||||
*/
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* 关联MCP工具ID列表
|
||||
*/
|
||||
@AutoMapping(target = "mcpToolIds", expression = "java(org.ruoyi.common.json.utils.JsonUtils.toJsonString(source.getMcpToolIds()))")
|
||||
private List<Long> mcpToolIds;
|
||||
|
||||
/**
|
||||
* 关联磁盘技能名列表
|
||||
*/
|
||||
@AutoMapping(target = "skillNames", expression = "java(org.ruoyi.common.json.utils.JsonUtils.toJsonString(source.getSkillNames()))")
|
||||
private List<String> skillNames;
|
||||
|
||||
/**
|
||||
* 关联知识库ID列表
|
||||
*/
|
||||
@AutoMapping(target = "knowledgeIds", expression = "java(org.ruoyi.common.json.utils.JsonUtils.toJsonString(source.getKnowledgeIds()))")
|
||||
private List<Long> knowledgeIds;
|
||||
|
||||
/**
|
||||
* 状态:0 正常 1 停用
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.ruoyi.domain.bo.media;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ImageGenerationRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "提示词不能为空")
|
||||
private String prompt;
|
||||
|
||||
private String size;
|
||||
|
||||
@Min(value = 0, message = "随机种子不能小于0")
|
||||
@Max(value = 2147483647, message = "随机种子不能大于2147483647")
|
||||
private Integer seed;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.ruoyi.domain.bo.media;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SpeechGenerationRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "输入文本不能为空")
|
||||
private String input;
|
||||
|
||||
private String voice;
|
||||
|
||||
private String responseFormat;
|
||||
|
||||
private Double speed;
|
||||
|
||||
private String instructions;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.ruoyi.domain.bo.media;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VideoGenerationRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "提示词不能为空")
|
||||
private String prompt;
|
||||
|
||||
private String size;
|
||||
|
||||
private Integer seconds;
|
||||
|
||||
private String quality;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.domain.entity.shortdrama.ShortDramaCharacterAppearance;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ShortDramaCharacterAppearance.class, reverseConvertGenerate = false)
|
||||
public class ShortDramaCharacterAppearanceBo extends BaseEntity {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long characterId;
|
||||
|
||||
private Integer appearanceIndex;
|
||||
|
||||
private String changeReason;
|
||||
|
||||
private String description;
|
||||
|
||||
private String referenceImageUrl;
|
||||
|
||||
private String imageUrls;
|
||||
|
||||
private String imageDescriptions;
|
||||
|
||||
private Integer selectedImageIndex;
|
||||
|
||||
private String previousImageUrls;
|
||||
|
||||
private String previousDescriptions;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.domain.entity.shortdrama.ShortDramaCharacter;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ShortDramaCharacter.class, reverseConvertGenerate = false)
|
||||
public class ShortDramaCharacterBo extends BaseEntity {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private String name;
|
||||
|
||||
private String aliases;
|
||||
|
||||
private String introduction;
|
||||
|
||||
private String roleLevel;
|
||||
|
||||
private String gender;
|
||||
|
||||
private String ageRange;
|
||||
|
||||
private String personalityTags;
|
||||
|
||||
private Integer costumeTier;
|
||||
|
||||
private String visualDescription;
|
||||
|
||||
private String referenceImageUrl;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ShortDramaComposeVideoBo {
|
||||
|
||||
@NotBlank(message = "转场类型不能为空")
|
||||
@Pattern(regexp = "none|dissolve|fade|slide", message = "不支持的转场类型")
|
||||
private String transitionType = "dissolve";
|
||||
|
||||
@NotNull(message = "转场时长不能为空")
|
||||
@DecimalMin(value = "0.0", message = "转场时长不能小于0秒")
|
||||
private BigDecimal transitionDurationSeconds = new BigDecimal("0.5");
|
||||
|
||||
@NotBlank(message = "成片画幅不能为空")
|
||||
@Pattern(regexp = "9:16|16:9|1:1", message = "不支持的成片画幅")
|
||||
private String aspectRatio = "9:16";
|
||||
@Size(min = 2, message = "至少选择2个分镜视频")
|
||||
private List<Long> storyboardIds;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ShortDramaIdeaBo {
|
||||
|
||||
@NotBlank(message = "创意想法不能为空")
|
||||
private String idea;
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
private String projectName;
|
||||
|
||||
private String artStyle;
|
||||
|
||||
private String aspectRatio;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.domain.entity.shortdrama.ShortDramaLocation;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ShortDramaLocation.class, reverseConvertGenerate = false)
|
||||
public class ShortDramaLocationBo extends BaseEntity {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private String name;
|
||||
|
||||
private String summary;
|
||||
|
||||
private Boolean hasCrowd;
|
||||
|
||||
private String crowdDescription;
|
||||
|
||||
private String availableSlots;
|
||||
|
||||
private String descriptions;
|
||||
|
||||
private String referenceImageUrl;
|
||||
|
||||
private String imageUrls;
|
||||
|
||||
private String imageDescriptions;
|
||||
|
||||
private Integer selectedImageIndex;
|
||||
|
||||
private String previousImageUrls;
|
||||
|
||||
private String previousDescriptions;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.domain.entity.shortdrama.ShortDramaProject;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ShortDramaProject.class, reverseConvertGenerate = false)
|
||||
public class ShortDramaProjectBo extends BaseEntity {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String projectName;
|
||||
|
||||
private String description;
|
||||
|
||||
private String status;
|
||||
|
||||
private String artStyle;
|
||||
|
||||
private String composeAspectRatio;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.mybatis.core.domain.BaseEntity;
|
||||
import org.ruoyi.domain.entity.shortdrama.ShortDramaScript;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ShortDramaScript.class, reverseConvertGenerate = false)
|
||||
public class ShortDramaScriptBo extends BaseEntity {
|
||||
|
||||
private Long id;
|
||||
|
||||
private Long projectId;
|
||||
|
||||
private String scriptName;
|
||||
|
||||
private String scriptText;
|
||||
|
||||
private String outlineText;
|
||||
|
||||
private String tone;
|
||||
|
||||
private String sourceType;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.ruoyi.domain.bo.shortdrama;
|
||||
|
||||
import dev.langchain4j.model.output.structured.Description;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Phase 1 剧本打磨结构化响应 —— langchain4j AiServices 自动解析用
|
||||
*
|
||||
* @author ageerle
|
||||
*/
|
||||
@Data
|
||||
public class ShortDramaScriptResult {
|
||||
|
||||
@Description("项目名称(有吸引力的短剧名)")
|
||||
private String projectName;
|
||||
|
||||
@Description("一句话简介(20-50字)")
|
||||
private String description;
|
||||
|
||||
@Description("剧本名称")
|
||||
private String scriptName;
|
||||
|
||||
@Description("风格基调(如:都市甜宠/古装虐恋/悬疑惊悚/喜剧爽文)")
|
||||
private String tone;
|
||||
|
||||
@Description("剧情大纲(400-800字,完整故事线)")
|
||||
private String outlineText;
|
||||
|
||||
@Description("完整短剧文本(1000-3000字,标准剧本格式,含场景头、动作描述、对话)")
|
||||
private String scriptText;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user