Merge branch 'ageerle:main' into main

This commit is contained in:
xiaonieli7
2025-11-07 16:26:46 +08:00
committed by GitHub
33 changed files with 2824 additions and 136 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@@ -152,7 +152,7 @@
</td> </td>
<td align="center"> <td align="center">
<img width="200" height="200" alt="95e8b1b3baeadbd24650bfb974ca5a58" src="https://github.com/user-attachments/assets/2a346218-6388-484d-aa75-6e98942193f7" /><br> <img width="200" height="200" alt="95e8b1b3baeadbd24650bfb974ca5a58" src="https://github.com/user-attachments/assets/e08fb61a-fd23-4b16-9d7e-85576f01cb10" /><br>
<strong>微信技术交流群</strong><br> <strong>微信技术交流群</strong><br>
<em>技术讨论</em> <em>技术讨论</em>
</td> </td>

View File

@@ -0,0 +1,222 @@
## RuoYi-AI 后端部署教程Docker 部署版)
### 一、前置条件
在部署前,请确保系统已满足以下条件:
#### ✅ 系统环境要求
- 操作系统Linux / MacOS推荐 Linux 服务器)
- CPU4 核以上
- 内存:≥ 4GB
- 磁盘空间:≥ 10GB建议 20GB+
#### ✅ 已安装软件
- **Docker**
- **Docker Compose**
验证命令是否可用:
```
docker -v
docker compose version
```
若无输出或提示“command not found”请先安装 Docker 及 Compose。
------
### 二、目录结构配置
#### 1⃣ 创建部署目录
在目标服务器执行以下命令:
```
# 第一级目录
mkdir /ruoyi-ai
cd /ruoyi-ai
# 第二级目录
mkdir deploy
cd deploy
# 第三级目录
mkdir data mysql-init
# 第四级目录
mkdir logs minio minio-config mysql redis weaviate
```
> 💡 `data` 目录用于挂载容器运行期间生成的数据文件。
最终目录结构示例:
```
/ruoyi-ai
└── deploy
├── data/
├── mysql-init/
├── logs/
├── minio/
├── minio-config/
├── mysql/
├── redis/
├── weaviate/
```
------
### 三、上传配置文件
将以下配置文件上传到 `/ruoyi-ai/deploy` 目录:
- `docker-compose.yaml`
- `.env`
- `ruoyi-ai.sql`
- `Dockerfile`
> 📂 这些文件在项目目录 `/script/deploy/deploy` 下。
> 上传后请检查文件路径是否与上方目录结构一致。
------
### 四、构建 Jar 包
1. 打开 IDEA 或其他构建工具
2. 选择 **Maven 构建配置**,勾选 `prod` 环境,取消 `dev` 环境
3. 点击 `package` 进行打包
4. **注意:** 在构建前请将 `application-prod.yml` 拖入
`ruoyi-admin/src/main/resources` 目录中
构建完成后会在:
```
ruoyi-admin/target/ruoyi-admin.jar
```
生成打包文件。
------
### 五、上传 Jar 包至服务器
将生成的 `ruoyi-admin.jar` 上传到服务器 `/ruoyi-ai/deploy` 目录下。
确保与 `Dockerfile` 同目录。
------
### 六、构建 Docker 镜像
`Dockerfile` 内容如下:
```
FROM openjdk:17-jdk
RUN mkdir -p /ruoyi/server/logs \
/ruoyi/server/temp
WORKDIR /ruoyi/server
COPY ruoyi-admin.jar ruoyi-admin.jar
ENTRYPOINT ["java","-jar","ruoyi-admin.jar"]
```
`/ruoyi-ai/deploy` 目录执行以下命令:
```
# 构建镜像
docker build -t ruoyi-ai-backend:v20251013 .
# 查看镜像是否构建成功
docker image ls
```
然后在 `docker-compose.yaml` 文件中,将对应服务的镜像名修改为:
```
image: ruoyi-ai-backend:v20251013
```
------
### 七、启动容器服务
在启动前请确认:
- `.env` 中端口号、数据库密码、环境变量已正确配置
- `docker-compose.yaml` 中 MySQL 的端口已开放(用于导入数据)
如示例:
```
ports:
- "3306:3306"
```
#### 启动命令:
```
cd /ruoyi-ai/deploy
docker compose up -d
```
#### 查看运行状态:
```
docker compose ps
```
#### 查看日志:
```
docker logs -f <容器名称>
```
> ⚠️ 初次启动时可仅运行 `ruoyi-admin`(后端)模块,将前端 `ruoyi-web` 服务暂时注释,确认后端服务正常后再启用前端容器。
------
### 八、数据库初始化
启动 MySQL 容器后,执行以下操作:
```
docker exec -it <mysql_container_name> bash
mysql -uroot -p
source /docker-entrypoint-initdb.d/ruoyi-ai.sql;
```
或手动在客户端中导入 `/ruoyi-ai/deploy/ruoyi-ai.sql` 文件。
------
### 九、常用 Docker 命令
| 功能 | 命令 |
| ----------------- | --------------------------------- |
| 查看容器状态 | `docker ps -a` |
| 查看日志 | `docker logs -f <容器名>` |
| 停止服务 | `docker compose down` |
| 重启服务 | `docker compose restart` |
| 重新构建镜像 | `docker compose build --no-cache` |
| 清理无用镜像/容器 | `docker system prune -a` |
------
### 🔍 十、部署验证
1. 检查容器是否全部启动成功:
```
docker compose ps
```
2. 访问后端接口:
```
http://<服务器IP>:<后端端口>
```
3. 检查日志输出无异常。

View File

@@ -93,6 +93,37 @@ public class WorkflowService extends ServiceImpl<WorkflowMapper, Workflow> {
return changeWorkflowToDTO(workflow2); return changeWorkflowToDTO(workflow2);
} }
/**
* 获取当前用户可访问的工作流详情
*
* @param uuid 工作流唯一标识
* @return 工作流详情
*/
public WorkflowResp getDetail(String uuid) {
Workflow workflow = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_NOT_FOUND);
return changeWorkflowToDTO(workflow);
}
/**
* 获取公开工作流详情
*
* @param uuid 工作流唯一标识
* @return 工作流详情
*/
public WorkflowResp getPublicDetail(String uuid) {
Workflow workflow = ChainWrappers.lambdaQueryChain(baseMapper)
.eq(Workflow::getUuid, uuid)
.eq(Workflow::getIsDeleted, false)
.eq(Workflow::getIsPublic, true)
.eq(Workflow::getIsEnable, true)
.last("limit 1")
.one();
if (null == workflow) {
throw new BaseException(ErrorEnum.A_WF_NOT_FOUND.getInfo());
}
return changeWorkflowToDTO(workflow);
}
public Workflow getByUuid(String uuid) { public Workflow getByUuid(String uuid) {
return ChainWrappers.lambdaQueryChain(baseMapper) return ChainWrappers.lambdaQueryChain(baseMapper)
.eq(Workflow::getUuid, uuid) .eq(Workflow::getUuid, uuid)
@@ -149,7 +180,24 @@ public class WorkflowService extends ServiceImpl<WorkflowMapper, Workflow> {
userIds.add(source.getUserId()); userIds.add(source.getUserId());
return target; return target;
}); });
// fillUserInfos(userIds, result.getRecords()); return result;
}
public Page<WorkflowResp> search(String keyword, Integer currentPage, Integer pageSize) {
Page<Workflow> page = ChainWrappers.lambdaQueryChain(baseMapper)
.eq(Workflow::getIsDeleted, false)
.eq(Workflow::getIsEnable, true)
.like(StringUtils.isNotBlank(keyword), Workflow::getTitle, keyword)
.orderByDesc(Workflow::getUpdateTime)
.page(new Page<>(currentPage, pageSize));
Page<WorkflowResp> result = new Page<>();
List<Long> userIds = new ArrayList<>();
MPPageUtil.convertToPage(page, result, WorkflowResp.class, (source, target) -> {
fillNodesAndEdges(target);
userIds.add(source.getUserId());
return target;
});
return result; return result;
} }

View File

@@ -18,6 +18,8 @@
<properties> <properties>
<easyexcel.version>3.2.1</easyexcel.version> <easyexcel.version>3.2.1</easyexcel.version>
<jna.version>5.13.0</jna.version>
<java-websocket.version>1.5.5</java-websocket.version>
</properties> </properties>
<!-- 按照用户要求,不添加任何依赖 --> <!-- 按照用户要求,不添加任何依赖 -->
@@ -70,5 +72,24 @@
<groupId>org.ruoyi</groupId> <groupId>org.ruoyi</groupId>
<artifactId>ruoyi-common-excel</artifactId> <artifactId>ruoyi-common-excel</artifactId>
</dependency> </dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>${jna.version}</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>${java-websocket.version}</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,16 @@
package org.ruoyi.aihuman.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 映射/voice/**路径到classpath:/voice/目录
registry.addResourceHandler("/voice/**")
.addResourceLocations("classpath:/voice/")
.setCachePeriod(3600);
}
}

View File

@@ -0,0 +1,157 @@
package org.ruoyi.aihuman.controller;
import java.util.List;
import cn.dev33.satoken.annotation.SaIgnore;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.ruoyi.common.log.enums.OperatorType;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
import org.ruoyi.common.log.annotation.Log;
import org.ruoyi.common.web.core.BaseController;
import org.ruoyi.core.page.PageQuery;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.common.core.validate.AddGroup;
import org.ruoyi.common.core.validate.EditGroup;
import org.ruoyi.common.log.enums.BusinessType;
import org.ruoyi.common.excel.utils.ExcelUtil;
import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
import org.ruoyi.aihuman.domain.bo.AihumanRealConfigBo;
import org.ruoyi.aihuman.service.AihumanRealConfigService;
import org.ruoyi.core.page.TableDataInfo;
/**
* 真人交互数字人配置
*
* @author ageerle
* @date Tue Oct 21 11:46:52 GMT+08:00 2025
*/
//临时免登录
@SaIgnore
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/aihuman/aihumanRealConfig")
public class AihumanRealConfigController extends BaseController {
private final AihumanRealConfigService aihumanRealConfigService;
/**
* 查询真人交互数字人配置列表
*/
@SaCheckPermission("aihuman:aihumanRealConfig:list")
@GetMapping("/list")
public TableDataInfo<AihumanRealConfigVo> list(AihumanRealConfigBo bo, PageQuery pageQuery) {
return aihumanRealConfigService.queryPageList(bo, pageQuery);
}
/**
* 导出真人交互数字人配置列表
*/
@SaCheckPermission("aihuman:aihumanRealConfig:export")
@Log(title = "真人交互数字人配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(AihumanRealConfigBo bo, HttpServletResponse response) {
List<AihumanRealConfigVo> list = aihumanRealConfigService.queryList(bo);
ExcelUtil.exportExcel(list, "真人交互数字人配置", AihumanRealConfigVo.class, response);
}
/**
* 获取真人交互数字人配置详细信息
*
* @param id 主键
*/
@SaCheckPermission("aihuman:aihumanRealConfig:query")
@GetMapping("/{id}")
public R<AihumanRealConfigVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Integer id) {
return R.ok(aihumanRealConfigService.queryById(id));
}
/**
* 新增真人交互数字人配置
*/
@SaCheckPermission("aihuman:aihumanRealConfig:add")
@Log(title = "真人交互数字人配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody AihumanRealConfigBo bo) {
return toAjax(aihumanRealConfigService.insertByBo(bo));
}
/**
* 修改真人交互数字人配置
*/
@SaCheckPermission("aihuman:aihumanRealConfig:edit")
@Log(title = "真人交互数字人配置", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
return toAjax(aihumanRealConfigService.updateByBo(bo));
}
/**
* 删除真人交互数字人配置
*
* @param ids 主键串
*/
@SaCheckPermission("aihuman:aihumanRealConfig:remove")
@Log(title = "真人交互数字人配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Integer[] ids) {
return toAjax(aihumanRealConfigService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 1.执行以下命令:
* cd F:\Projects\AI-Human\LiveTalking
* conda activate D:\zg117\C\Users\zg117\.conda\envs\livetalking_new
* python app.py --transport webrtc --model wav2lip --avatar_id wav2lip256_avatar1
*
* 2.监听 python app.py --transport webrtc --model wav2lip --avatar_id wav2lip256_avatar1 执行情况
*
* 3.返回执行结果并打开页面
* http://127.0.0.1:8010/webrtcapi-diy.html
*/
@SaCheckPermission("aihuman:aihumanRealConfig:run")
//@Log(title = "真人交互数字人配置", businessType = BusinessType.UPDATE, operatorType = OperatorType.OTHER)
@RepeatSubmit()
@PutMapping("/run")
public R<String> run(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
boolean result = aihumanRealConfigService.runByBo(bo);
if (result) {
// 返回前端页面URL前端可以根据这个URL跳转或打开新页面
// http://127.0.0.1:8010/webrtcapi-diy.html 其中的 http://127.0.0.1 获取当前java服务的IP地址
// return R.ok("http://127.0.0.1:8010/webrtcapi-diy.html");
// 运行状态
bo.setRunStatus("1");
return R.ok("http://127.0.0.1:8010/webrtcapi-diy.html");
} else {
return R.fail("启动真人交互数字人失败");
}
}
/**
* 停止真人交互数字人配置任务
*/
@SaCheckPermission("aihuman:aihumanRealConfig:stop")
//@Log(title = "真人交互数字人配置", businessType = BusinessType.UPDATE, operatorType = OperatorType.OTHER)
@RepeatSubmit()
@PutMapping("/stop")
public R<String> stop(@Validated(EditGroup.class) @RequestBody AihumanRealConfigBo bo) {
boolean result = aihumanRealConfigService.stopByBo(bo);
if (result) {
// 运行状态
bo.setRunStatus("0");
return R.ok("真人交互数字人任务已停止");
} else {
return R.fail("停止真人交互数字人任务失败或没有正在运行的任务");
}
}
}

View File

@@ -0,0 +1,101 @@
package org.ruoyi.aihuman.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.io.Serializable;
/**
* 真人交互数字人配置对象 aihuman_real_config
*
* @author ageerle
* @date Tue Oct 21 11:46:52 GMT+08:00 2025
*/
@Data
@TableName("aihuman_real_config")
public class AihumanRealConfig implements Serializable {
/**
* 主键id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 场景名称
*/
private String name;
/**
* 真人形象名称
*/
private String avatars;
/**
* 模型名称
*/
private String models;
/**
* 形象参数(预留)
*/
private String avatarsParams;
/**
* 模型参数(预留)
*/
private String modelsParams;
/**
* 智能体参数(扣子)
*/
private String agentParams;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 状态
*/
private Integer status;
/**
* 发布状态
*/
private Integer publish;
/**
* 运行参数
*/
private String runParams;
/**
* 运行状态
*/
private String runStatus;
/**
* 创建部门
*/
private String createDept;
/**
* 创建用户
*/
private String createBy;
/**
* 更新用户
*/
private String updateBy;
}

View File

@@ -0,0 +1,21 @@
package org.ruoyi.aihuman.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 语音请求参数实体类
*/
@Data
public class VoiceRequest {
@JsonProperty("ENDPOINT")
private String endpoint;
private String appId;
private String accessToken;
private String resourceId;
private String voice;
private String text;
private String encoding;
}

View File

@@ -0,0 +1,87 @@
package org.ruoyi.aihuman.domain.bo;
import org.ruoyi.aihuman.domain.AihumanRealConfig;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.time.LocalDateTime;
import java.io.Serializable;
/**
* 真人交互数字人配置业务对象 aihuman_real_config
*
* @author ageerle
* @date Tue Oct 21 11:46:52 GMT+08:00 2025
*/
@Data
@AutoMapper(target = AihumanRealConfig.class, reverseConvertGenerate = false)
public class AihumanRealConfigBo implements Serializable {
private Integer id;
/**
* 场景名称
*/
private String name;
/**
* 真人形象名称
*/
private String avatars;
/**
* 模型名称
*/
private String models;
/**
* 形象参数(预留)
*/
private String avatarsParams;
/**
* 模型参数(预留)
*/
private String modelsParams;
/**
* 智能体参数(扣子)
*/
private String agentParams;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 状态
*/
private Integer status;
/**
* 发布状态
*/
private Integer publish;
/**
* 运行参数
*/
private String runParams;
/**
* 运行状态
*/
private String runStatus;
/**
* 创建部门
*/
private String createDept;
/**
* 创建用户
*/
private String createBy;
/**
* 更新用户
*/
private String updateBy;
}

View File

@@ -0,0 +1,108 @@
package org.ruoyi.aihuman.domain.vo;
import java.time.LocalDateTime;
import java.io.Serializable;
import org.ruoyi.aihuman.domain.AihumanRealConfig;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.ruoyi.common.excel.annotation.ExcelDictFormat;
import org.ruoyi.common.excel.convert.ExcelDictConvert;
/**
* 真人交互数字人配置视图对象 aihuman_real_config
*
* @author ageerle
* @date Tue Oct 21 11:46:52 GMT+08:00 2025
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = AihumanRealConfig.class)
public class AihumanRealConfigVo implements Serializable {
private Integer id;
/**
* 场景名称
*/
@ExcelProperty(value = "场景名称")
private String name;
/**
* 真人形象名称
*/
@ExcelProperty(value = "真人形象名称")
private String avatars;
/**
* 模型名称
*/
@ExcelProperty(value = "模型名称")
private String models;
/**
* 形象参数(预留)
*/
@ExcelProperty(value = "形象参数", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
private String avatarsParams;
/**
* 模型参数(预留)
*/
@ExcelProperty(value = "模型参数", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
private String modelsParams;
/**
* 智能体参数(扣子)
*/
@ExcelProperty(value = "智能体参数", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "$column.readConverterExp()")
private String agentParams;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
@ExcelProperty(value = "更新时间")
private LocalDateTime updateTime;
/**
* 状态
*/
@ExcelProperty(value = "状态")
private Integer status;
/**
* 发布状态
*/
@ExcelProperty(value = "发布状态")
private Integer publish;
/**
* 运行参数
*/
@ExcelProperty(value = "运行参数")
private String runParams;
/**
* 运行状态
*/
@ExcelProperty(value = "运行状态")
private String runStatus;
/**
* 创建部门
*/
@ExcelProperty(value = "创建部门")
private String createDept;
/**
* 创建用户
*/
@ExcelProperty(value = "创建用户")
private String createBy;
/**
* 更新用户
*/
@ExcelProperty(value = "更新用户")
private String updateBy;
}

View File

@@ -0,0 +1,17 @@
package org.ruoyi.aihuman.mapper;
import org.ruoyi.aihuman.domain.AihumanRealConfig;
import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
import org.ruoyi.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Mapper;
/**
* 真人交互数字人配置Mapper接口
*
* @author ageerle
* @date Tue Oct 21 11:46:52 GMT+08:00 2025
*/
@Mapper
public interface AihumanRealConfigMapper extends BaseMapperPlus<AihumanRealConfig, AihumanRealConfigVo> {
}

View File

@@ -0,0 +1,26 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum CompressionBits {
None_((byte) 0),
Gzip((byte) 0b1),
Custom((byte) 0b11),
;
private final byte value;
CompressionBits(byte b) {
this.value = b;
}
public static CompressionBits fromValue(int value) {
for (CompressionBits type : CompressionBits.values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown CompressionBits value: " + value);
}
}

View File

@@ -0,0 +1,90 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum EventType {
// Default event
NONE(0),
// Upstream Connection events (1-49)
START_CONNECTION(1),
START_TASK(1),
FINISH_CONNECTION(2),
FINISH_TASK(2),
// Downstream Connection events (50-99)
CONNECTION_STARTED(50),
TASK_STARTED(50),
CONNECTION_FAILED(51),
TASK_FAILED(51),
CONNECTION_FINISHED(52),
TASK_FINISHED(52),
// Upstream Session events (100-149)
START_SESSION(100),
CANCEL_SESSION(101),
FINISH_SESSION(102),
// Downstream Session events (150-199)
SESSION_STARTED(150),
SESSION_CANCELED(151),
SESSION_FINISHED(152),
SESSION_FAILED(153),
USAGE_RESPONSE(154),
CHARGE_DATA(154),
// Upstream General events (200-249)
TASK_REQUEST(200),
UPDATE_CONFIG(201),
// Downstream General events (250-299)
AUDIO_MUTED(250),
// Upstream TTS events (300-349)
SAY_HELLO(300),
// Downstream TTS events (350-399)
TTS_SENTENCE_START(350),
TTS_SENTENCE_END(351),
TTS_RESPONSE(352),
TTS_ENDED(359),
PODCAST_ROUND_START(360),
PODCAST_ROUND_RESPONSE(361),
PODCAST_ROUND_END(362),
// Downstream ASR events (450-499)
ASR_INFO(450),
ASR_RESPONSE(451),
ASR_ENDED(459),
// Upstream Chat events (500-549)
CHAT_TTS_TEXT(500),
// Downstream Chat events (550-599)
CHAT_RESPONSE(550),
CHAT_ENDED(559),
// Subtitle events (650-699)
SOURCE_SUBTITLE_START(650),
SOURCE_SUBTITLE_RESPONSE(651),
SOURCE_SUBTITLE_END(652),
TRANSLATION_SUBTITLE_START(653),
TRANSLATION_SUBTITLE_RESPONSE(654),
TRANSLATION_SUBTITLE_END(655);
private final int value;
EventType(int value) {
this.value = value;
}
public static EventType fromValue(int value) {
for (EventType type : EventType.values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown EventType value: " + value);
}
}

View File

@@ -0,0 +1,27 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum HeaderSizeBits {
HeaderSize4((byte) 1),
HeaderSize8((byte) 2),
HeaderSize12((byte) 3),
HeaderSize16((byte) 4),
;
private final byte value;
HeaderSizeBits(byte b) {
this.value = b;
}
public static HeaderSizeBits fromValue(int value) {
for (HeaderSizeBits type : HeaderSizeBits.values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown HeaderSizeBits value: " + value);
}
}

View File

@@ -0,0 +1,220 @@
package org.ruoyi.aihuman.protocol;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
@Slf4j
@Data
public class Message {
private byte version = VersionBits.Version1.getValue();
private byte headerSize = HeaderSizeBits.HeaderSize4.getValue();
private MsgType type;
private MsgTypeFlagBits flag;
private byte serialization = SerializationBits.JSON.getValue();
private byte compression = 0;
private EventType event;
private String sessionId;
private String connectId;
private int sequence;
private int errorCode;
private byte[] payload;
public Message(MsgType type, MsgTypeFlagBits flag) {
this.type = type;
this.flag = flag;
}
public static Message unmarshal(byte[] data) throws Exception {
ByteBuffer buffer = ByteBuffer.wrap(data);
byte type_and_flag = data[1];
MsgType type = MsgType.fromValue((type_and_flag >> 4) & 0x0F);
MsgTypeFlagBits flag = MsgTypeFlagBits.fromValue(type_and_flag & 0x0F);
// Read version and header size
int versionAndHeaderSize = buffer.get();
VersionBits version = VersionBits.fromValue((versionAndHeaderSize >> 4) & 0x0F);
HeaderSizeBits headerSize = HeaderSizeBits.fromValue(versionAndHeaderSize & 0x0F);
// Skip second byte
buffer.get();
// Read serialization and compression method
int serializationCompression = buffer.get();
SerializationBits serialization = SerializationBits.fromValue((serializationCompression >> 4) & 0x0F);
CompressionBits compression = CompressionBits.fromValue(serializationCompression & 0x0F);
// Skip padding bytes
int headerSizeInt = 4 * (int) headerSize.getValue();
int paddingSize = headerSizeInt - 3;
while (paddingSize > 0) {
buffer.get();
paddingSize -= 1;
}
Message message = new Message(type, flag);
message.setVersion(version.getValue());
message.setHeaderSize(headerSize.getValue());
message.setSerialization(serialization.getValue());
message.setCompression(compression.getValue());
// Read sequence if present
if (flag == MsgTypeFlagBits.POSITIVE_SEQ || flag == MsgTypeFlagBits.NEGATIVE_SEQ) {
// Read 4 bytes from ByteBuffer and parse as int (big-endian)
byte[] sequeueBytes = new byte[4];
if (buffer.remaining() >= 4) {
buffer.get(sequeueBytes); // Read 4 bytes into array
ByteBuffer wrapper = ByteBuffer.wrap(sequeueBytes);
wrapper.order(ByteOrder.BIG_ENDIAN); // Set big-endian order
message.setSequence(wrapper.getInt());
}
}
// Read event if present
if (flag == MsgTypeFlagBits.WITH_EVENT) {
// Read 4 bytes from ByteBuffer and parse as int (big-endian)
byte[] eventBytes = new byte[4];
if (buffer.remaining() >= 4) {
buffer.get(eventBytes); // Read 4 bytes into array
ByteBuffer wrapper = ByteBuffer.wrap(eventBytes);
wrapper.order(ByteOrder.BIG_ENDIAN); // Set big-endian order
message.setEvent(EventType.fromValue(wrapper.getInt()));
}
if (type != MsgType.ERROR && !(message.event == EventType.START_CONNECTION
|| message.event == EventType.FINISH_CONNECTION ||
message.event == EventType.CONNECTION_STARTED
|| message.event == EventType.CONNECTION_FAILED ||
message.event == EventType.CONNECTION_FINISHED)) {
// Read sessionId if present
int sessionIdLength = buffer.getInt();
if (sessionIdLength > 0) {
byte[] sessionIdBytes = new byte[sessionIdLength];
buffer.get(sessionIdBytes);
message.setSessionId(new String(sessionIdBytes, StandardCharsets.UTF_8));
}
}
if (message.event == EventType.CONNECTION_STARTED || message.event == EventType.CONNECTION_FAILED
|| message.event == EventType.CONNECTION_FINISHED) {
// Read connectId if present
int connectIdLength = buffer.getInt();
if (connectIdLength > 0) {
byte[] connectIdBytes = new byte[connectIdLength];
buffer.get(connectIdBytes);
message.setConnectId(new String(connectIdBytes, StandardCharsets.UTF_8));
}
}
}
// Read errorCode if present
if (type == MsgType.ERROR) {
// Read 4 bytes from ByteBuffer and parse as int (big-endian)
byte[] errorCodeBytes = new byte[4];
if (buffer.remaining() >= 4) {
buffer.get(errorCodeBytes); // Read 4 bytes into array
ByteBuffer wrapper = ByteBuffer.wrap(errorCodeBytes);
wrapper.order(ByteOrder.BIG_ENDIAN); // Set big-endian order
message.setErrorCode(wrapper.getInt());
}
}
// Read remaining bytes as payload
if (buffer.remaining() > 0) {
// 4 bytes length
int payloadLength = buffer.getInt();
if (payloadLength > 0) {
byte[] payloadBytes = new byte[payloadLength];
buffer.get(payloadBytes);
message.setPayload(payloadBytes);
}
}
return message;
}
public byte[] marshal() throws Exception {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
// Write header
buffer.write((version & 0x0F) << 4 | (headerSize & 0x0F));
buffer.write((type.getValue() & 0x0F) << 4 | (flag.getValue() & 0x0F));
buffer.write((serialization & 0x0F) << 4 | (compression & 0x0F));
int headerSizeInt = 4 * (int) headerSize;
int padding = headerSizeInt - buffer.size();
while (padding > 0) {
buffer.write(0);
padding -= 1;
}
// Write event if present
if (event != null) {
byte[] eventBytes = ByteBuffer.allocate(4).putInt(event.getValue()).array();
buffer.write(eventBytes);
}
// Write sessionId if present
if (sessionId != null) {
byte[] sessionIdBytes = sessionId.getBytes(StandardCharsets.UTF_8);
buffer.write(ByteBuffer.allocate(4).putInt(sessionIdBytes.length).array());
buffer.write(sessionIdBytes);
}
// Write connectId if present
if (connectId != null) {
byte[] connectIdBytes = connectId.getBytes(StandardCharsets.UTF_8);
buffer.write(ByteBuffer.allocate(4).putInt(connectIdBytes.length).array());
buffer.write(connectIdBytes);
}
// Write sequence if present
if (sequence != 0) {
buffer.write(ByteBuffer.allocate(4).putInt(sequence).array());
}
// Write errorCode if present
if (errorCode != 0) {
buffer.write(ByteBuffer.allocate(4).putInt(errorCode).array());
}
// Write payload if present
if (payload != null && payload.length > 0) {
buffer.write(ByteBuffer.allocate(4).putInt(payload.length).array());
buffer.write(payload);
}
return buffer.toByteArray();
}
@Override
public String toString() {
switch (this.type) {
case AUDIO_ONLY_SERVER:
case AUDIO_ONLY_CLIENT:
if (this.flag == MsgTypeFlagBits.POSITIVE_SEQ || this.flag == MsgTypeFlagBits.NEGATIVE_SEQ) {
return String.format("MsgType: %s, EventType: %s, Sequence: %d, PayloadSize: %d", this.type, this.event, this.sequence,
this.payload != null ? this.payload.length : 0);
}
return String.format("MsgType: %s, EventType: %s, PayloadSize: %d", this.type, this.event,
this.payload != null ? this.payload.length : 0);
case ERROR:
return String.format("MsgType: %s, EventType: %s, ErrorCode: %d, Payload: %s", this.type, this.event, this.errorCode,
this.payload != null ? new String(this.payload) : "null");
default:
if (this.flag == MsgTypeFlagBits.POSITIVE_SEQ || this.flag == MsgTypeFlagBits.NEGATIVE_SEQ) {
return String.format("MsgType: %s, EventType: %s, Sequence: %d, Payload: %s",
this.type, this.event, this.sequence,
this.payload != null ? new String(this.payload) : "null");
}
return String.format("MsgType: %s, EventType: %s, Payload: %s", this.type, this.event,
this.payload != null ? new String(this.payload) : "null");
}
}
}

View File

@@ -0,0 +1,29 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum MsgType {
INVALID((byte) 0),
FULL_CLIENT_REQUEST((byte) 0b1),
AUDIO_ONLY_CLIENT((byte) 0b10),
FULL_SERVER_RESPONSE((byte) 0b1001),
AUDIO_ONLY_SERVER((byte) 0b1011),
FRONT_END_RESULT_SERVER((byte) 0b1100),
ERROR((byte) 0b1111);
private final byte value;
MsgType(byte value) {
this.value = value;
}
public static MsgType fromValue(int value) {
for (MsgType type : MsgType.values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown MsgType value: " + value);
}
}

View File

@@ -0,0 +1,27 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum MsgTypeFlagBits {
NO_SEQ((byte) 0), // Non-terminating packet without sequence number
POSITIVE_SEQ((byte) 0b1), // Non-terminating packet with positive sequence number
LAST_NO_SEQ((byte) 0b10), // Terminating packet without sequence number
NEGATIVE_SEQ((byte) 0b11), // Terminating packet with negative sequence number
WITH_EVENT((byte) 0b100); // Packet containing event number
private final byte value;
MsgTypeFlagBits(byte value) {
this.value = value;
}
public static MsgTypeFlagBits fromValue(int value) {
for (MsgTypeFlagBits flag : MsgTypeFlagBits.values()) {
if (flag.value == value) {
return flag;
}
}
throw new IllegalArgumentException("Unknown MsgTypeFlagBits value: " + value);
}
}

View File

@@ -0,0 +1,27 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum SerializationBits {
Raw((byte) 0),
JSON((byte) 0b1),
Thrift((byte) 0b11),
Custom((byte) 0b1111),
;
private final byte value;
SerializationBits(byte b) {
this.value = b;
}
public static SerializationBits fromValue(int value) {
for (SerializationBits type : SerializationBits.values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown SerializationBits value: " + value);
}
}

View File

@@ -0,0 +1,115 @@
package org.ruoyi.aihuman.protocol;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@Slf4j
public class SpeechWebSocketClient extends WebSocketClient {
private final BlockingQueue<Message> messageQueue = new LinkedBlockingQueue<>();
public SpeechWebSocketClient(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
log.info("WebSocket connection established, Logid: {}", handshakedata.getFieldValue("x-tt-logid"));
}
@Override
public void onMessage(String message) {
log.warn("Received unexpected text message: {}", message);
}
@Override
public void onMessage(ByteBuffer bytes) {
try {
Message message = Message.unmarshal(bytes.array());
messageQueue.put(message);
} catch (Exception e) {
log.error("Failed to parse message", e);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
log.info("WebSocket connection closed: code={}, reason={}, remote={}", code, reason, remote);
}
@Override
public void onError(Exception ex) {
log.error("WebSocket error", ex);
}
public void sendStartConnection() throws Exception {
Message message = new Message(MsgType.FULL_CLIENT_REQUEST, MsgTypeFlagBits.WITH_EVENT);
message.setEvent(EventType.START_CONNECTION);
message.setPayload("{}".getBytes());
sendMessage(message);
}
public void sendFinishConnection() throws Exception {
Message message = new Message(MsgType.FULL_CLIENT_REQUEST, MsgTypeFlagBits.WITH_EVENT);
message.setEvent(EventType.FINISH_CONNECTION);
sendMessage(message);
}
public void sendStartSession(byte[] payload, String sessionId) throws Exception {
Message message = new Message(MsgType.FULL_CLIENT_REQUEST, MsgTypeFlagBits.WITH_EVENT);
message.setEvent(EventType.START_SESSION);
message.setSessionId(sessionId);
message.setPayload(payload);
sendMessage(message);
}
public void sendFinishSession(String sessionId) throws Exception {
Message message = new Message(MsgType.FULL_CLIENT_REQUEST, MsgTypeFlagBits.WITH_EVENT);
message.setEvent(EventType.FINISH_SESSION);
message.setSessionId(sessionId);
message.setPayload("{}".getBytes());
sendMessage(message);
}
public void sendTaskRequest(byte[] payload, String sessionId) throws Exception {
Message message = new Message(MsgType.FULL_CLIENT_REQUEST, MsgTypeFlagBits.WITH_EVENT);
message.setEvent(EventType.TASK_REQUEST);
message.setSessionId(sessionId);
message.setPayload(payload);
sendMessage(message);
}
public void sendFullClientMessage(byte[] payload) throws Exception {
Message message = new Message(MsgType.FULL_CLIENT_REQUEST, MsgTypeFlagBits.NO_SEQ);
message.setPayload(payload);
sendMessage(message);
}
public void sendMessage(Message message) throws Exception {
log.info("Send: {}", message);
send(message.marshal());
}
public Message receiveMessage() throws InterruptedException {
Message message = messageQueue.take();
log.info("Receive: {}", message);
return message;
}
public Message waitForMessage(MsgType type, EventType event) throws InterruptedException {
while (true) {
Message message = receiveMessage();
if (message.getType() == type && message.getEvent() == event) {
return message;
} else {
throw new RuntimeException("Unexpected message: " + message);
}
}
}
}

View File

@@ -0,0 +1,27 @@
package org.ruoyi.aihuman.protocol;
import lombok.Getter;
@Getter
public enum VersionBits {
Version1((byte) 1),
Version2((byte) 2),
Version3((byte) 3),
Version4((byte) 4),
;
private final byte value;
VersionBits(byte b) {
this.value = b;
}
public static VersionBits fromValue(int value) {
for (VersionBits type : VersionBits.values()) {
if (type.value == value) {
return type;
}
}
throw new IllegalArgumentException("Unknown VersionBits value: " + value);
}
}

View File

@@ -0,0 +1,56 @@
package org.ruoyi.aihuman.service;
import org.ruoyi.aihuman.domain.vo.AihumanRealConfigVo;
import org.ruoyi.aihuman.domain.bo.AihumanRealConfigBo;
import org.ruoyi.core.page.TableDataInfo;
import org.ruoyi.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 真人交互数字人配置Service接口
*
* @author ageerle
* @date Tue Oct 21 11:46:52 GMT+08:00 2025
*/
public interface AihumanRealConfigService {
/**
* 查询真人交互数字人配置
*/
AihumanRealConfigVo queryById(Integer id);
/**
* 查询真人交互数字人配置列表
*/
TableDataInfo<AihumanRealConfigVo> queryPageList(AihumanRealConfigBo bo, PageQuery pageQuery);
/**
* 查询真人交互数字人配置列表
*/
List<AihumanRealConfigVo> queryList(AihumanRealConfigBo bo);
/**
* 新增真人交互数字人配置
*/
Boolean insertByBo(AihumanRealConfigBo bo);
/**
* 修改真人交互数字人配置
*/
Boolean updateByBo(AihumanRealConfigBo bo);
/**
* 执行真人交互数字人配置
*/
Boolean runByBo(AihumanRealConfigBo bo);
/**
* 校验并批量删除真人交互数字人配置信息
*/
Boolean deleteWithValidByIds(Collection<Integer> ids, Boolean isValid);
// 在AihumanRealConfigService接口中添加
Boolean stopByBo(AihumanRealConfigBo bo);
}

View File

@@ -0,0 +1,4 @@
package org.ruoyi.aihuman.service;
public interface AihumanVolcengineService {
}

View File

@@ -0,0 +1,4 @@
package org.ruoyi.aihuman.service.impl;
public class AihumanVolcengineServiceImpl {
}

View File

@@ -0,0 +1,19 @@
-- 菜单 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1980480880138051584, '真人交互数字人配置', '2000', '1', 'aihumanRealConfig', 'aihuman/aihumanRealConfig/index', 1, 0, 'C', '0', '0', 'aihuman:aihumanRealConfig:list', '#', 103, 1, sysdate(), null, null, '真人交互数字人配置菜单');
-- 按钮 SQL
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1980480880138051585, '真人交互数字人配置查询', 1980480880138051584, '1', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:query', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1980480880138051586, '真人交互数字人配置新增', 1980480880138051584, '2', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:add', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1980480880138051587, '真人交互数字人配置修改', 1980480880138051584, '3', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:edit', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1980480880138051588, '真人交互数字人配置删除', 1980480880138051584, '4', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:remove', '#', 103, 1, sysdate(), null, null, '');
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(1980480880138051589, '真人交互数字人配置导出', 1980480880138051584, '5', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanRealConfig:export', '#', 103, 1, sysdate(), null, null, '');

View File

@@ -0,0 +1,160 @@
package org.ruoyi.aihuman.volcengine;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ruoyi.aihuman.protocol.EventType;
import org.ruoyi.aihuman.protocol.Message;
import org.ruoyi.aihuman.protocol.MsgType;
import org.ruoyi.aihuman.protocol.SpeechWebSocketClient;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.net.URI;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Slf4j
public class Bidirection {
private static final String ENDPOINT = "wss://openspeech.bytedance.com/api/v3/tts/bidirection";
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* Get resource ID based on voice type
*
* @param voice Voice type string
* @return Corresponding resource ID
*/
public static String voiceToResourceId(String voice) {
// Map different voice types to resource IDs based on actual needs
if (voice.startsWith("S_")) {
return "volc.megatts.default";
}
return "volc.service_type.10029";
}
public static void main(String[] args) throws Exception {
// Configure parameters
String appId = System.getProperty("appId", "1055299334");
String accessToken = System.getProperty("accessToken", "fOHuq4R4dirMYiOruCU3Ek9q75zV0KVW");
String resourceId = System.getProperty("resourceId", "seed-tts-2.0");
String voice = System.getProperty("voice", "zh_female_vv_uranus_bigtts");
String text = System.getProperty("text", "你好呀我是AI合成的语音很高兴认识你。");
String encoding = System.getProperty("encoding", "mp3");
if (appId.isEmpty() || accessToken.isEmpty()) {
throw new IllegalArgumentException("Please set appId and accessToken system properties");
}
// Set request headers
Map<String, String> headers = Map.of(
"X-Api-App-Key", appId,
"X-Api-Access-Key", accessToken,
"X-Api-Resource-Id", resourceId.isEmpty() ? voiceToResourceId(voice) : resourceId,
"X-Api-Connect-Id", UUID.randomUUID().toString());
// Create WebSocket client
SpeechWebSocketClient client = new SpeechWebSocketClient(new URI(ENDPOINT), headers);
try {
client.connectBlocking();
Map<String, Object> request = Map.of(
"user", Map.of("uid", UUID.randomUUID().toString()),
"namespace", "BidirectionalTTS",
"req_params", Map.of(
"speaker", voice,
"audio_params", Map.of(
"format", encoding,
"sample_rate", 24000,
"enable_timestamp", true),
// additions requires a JSON string
"additions", objectMapper.writeValueAsString(Map.of(
"disable_markdown_filter", false))));
// Start connection
client.sendStartConnection();
// Wait for connection started
client.waitForMessage(MsgType.FULL_SERVER_RESPONSE, EventType.CONNECTION_STARTED);
// Process each sentence
String[] sentences = text.split("");
boolean audioReceived = false;
for (int i = 0; i < sentences.length; i++) {
if (sentences[i].trim().isEmpty()) {
continue;
}
String sessionId = UUID.randomUUID().toString();
ByteArrayOutputStream audioStream = new ByteArrayOutputStream();
// Start session
Map<String, Object> startReq = Map.of(
"user", request.get("user"),
"namespace", request.get("namespace"),
"req_params", request.get("req_params"),
"event", EventType.START_SESSION.getValue());
client.sendStartSession(objectMapper.writeValueAsBytes(startReq), sessionId);
// Wait for session started
client.waitForMessage(MsgType.FULL_SERVER_RESPONSE, EventType.SESSION_STARTED);
// Send text
for (char c : sentences[i].toCharArray()) {
// Create new req_params with text
@SuppressWarnings("unchecked")
Map<String, Object> currentReqParams = new HashMap<>(
(Map<String, Object>) request.get("req_params"));
currentReqParams.put("text", String.valueOf(c));
// Create current request
Map<String, Object> currentRequest = Map.of(
"user", request.get("user"),
"namespace", request.get("namespace"),
"req_params", currentReqParams,
"event", EventType.TASK_REQUEST.getValue());
client.sendTaskRequest(objectMapper.writeValueAsBytes(currentRequest), sessionId);
}
// End session
client.sendFinishSession(sessionId);
// Receive response
while (true) {
Message msg = client.receiveMessage();
switch (msg.getType()) {
case FULL_SERVER_RESPONSE:
break;
case AUDIO_ONLY_SERVER:
if (!audioReceived && audioStream.size() > 0) {
audioReceived = true;
}
if (msg.getPayload() != null) {
audioStream.write(msg.getPayload());
}
break;
default:
throw new RuntimeException("Unexpected message: " + msg);
}
if (msg.getEvent() == EventType.SESSION_FINISHED) {
break;
}
}
if (audioStream.size() > 0) {
String fileName = String.format("%s_session_%d.%s", voice, i, encoding);
Files.write(new File(fileName).toPath(), audioStream.toByteArray());
log.info("Audio saved to file: {}", fileName);
}
}
if (!audioReceived) {
throw new RuntimeException("No audio data received");
}
// End connection
client.sendFinishConnection();
} finally {
client.closeBlocking();
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.ruoyi.aihuman.mapper.AihumanRealConfigMapper">
</mapper>

View File

@@ -3,8 +3,8 @@ gen:
# 作者 # 作者
author: ageerle author: ageerle
# 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool # 默认生成包路径 system 需改成自己的模块名称 如 system monitor tool
packageName: org.ruoyi.system packageName: org.ruoyi.aihuman
# 自动去除表前缀默认是false # 自动去除表前缀默认是false
autoRemovePre: false autoRemovePre: false
# 表前缀(生成类名不会包含表前缀,多个用逗号分隔) # 表前缀(生成类名不会包含表前缀,多个用逗号分隔)
tablePrefix: sys_ tablePrefix: aihuman_

View File

@@ -83,6 +83,17 @@ public class WorkflowController {
return R.ok(workflowService.search(keyword, isPublic, null, currentPage, pageSize)); return R.ok(workflowService.search(keyword, isPublic, null, currentPage, pageSize));
} }
/**
* 获取当前用户可访问的工作流详情
*
* @param uuid 工作流唯一标识
* @return 工作流详情
*/
@GetMapping("/{uuid}")
public R<WorkflowResp> getDetail(@PathVariable String uuid) {
return R.ok(workflowService.getDetail(uuid));
}
/** /**
* 搜索公开工作流 * 搜索公开工作流
* *
@@ -98,6 +109,33 @@ public class WorkflowController {
return R.ok(workflowService.searchPublic(keyword, currentPage, pageSize)); return R.ok(workflowService.searchPublic(keyword, currentPage, pageSize));
} }
/**
* 搜索公开工作流
*
* @param keyword 搜索关键词
* @param currentPage 当前页数
* @param pageSize 每页数量
* @return 工作流列表
*/
@GetMapping("/search")
public R<Page<WorkflowResp>> search(@RequestParam(defaultValue = "") String keyword,
@NotNull @Min(1) Integer currentPage,
@NotNull @Min(10) Integer pageSize) {
return R.ok(workflowService.search(keyword, currentPage, pageSize));
}
/**
* 获取公开工作流详情
*
* @param uuid 工作流唯一标识
* @return 工作流详情
*/
@GetMapping("/public/{uuid}")
public R<WorkflowResp> getPublicDetail(@PathVariable String uuid) {
return R.ok(workflowService.getPublicDetail(uuid));
}
@GetMapping("/public/operators") @GetMapping("/public/operators")
public R<List<Map<String, String>>> searchPublic() { public R<List<Map<String, String>>> searchPublic() {
List<Map<String, String>> result = new ArrayList<>(); List<Map<String, String>> result = new ArrayList<>();

View File

@@ -1,115 +1,114 @@
CREATE TABLE t_workflow CREATE TABLE `t_workflow`
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY, `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
uuid VARCHAR(32) NOT NULL DEFAULT '', `uuid` varchar(32) NOT NULL DEFAULT 'uuid',
title VARCHAR(100) NOT NULL DEFAULT '', `title` varchar(100) NOT NULL DEFAULT '标题',
remark TEXT NOT NULL, `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户ID',
user_id BIGINT NOT NULL DEFAULT 0, `is_public` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否公开',
is_public TINYINT(1) NOT NULL DEFAULT 0, `is_enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
is_enable TINYINT(1) NOT NULL DEFAULT 1, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `remark` text COMMENT '备注',
is_deleted TINYINT(1) NOT NULL DEFAULT 0 `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除 默认0不删除',
) ENGINE = InnoDB PRIMARY KEY (`id`)
DEFAULT CHARSET = utf8mb4 ) ENGINE=InnoDB AUTO_INCREMENT=119 DEFAULT CHARSET=utf8mb4 COMMENT='工作流定义(用户定义的工作流)| Workflow Definition';
COMMENT ='工作流定义(用户定义的工作流)| Workflow Definition';
CREATE TABLE t_workflow_node
CREATE TABLE `t_workflow_node`
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY, `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
uuid VARCHAR(32) NOT NULL DEFAULT '', `uuid` varchar(32) NOT NULL DEFAULT '' COMMENT '节点唯一标识',
workflow_id BIGINT NOT NULL DEFAULT 0, `workflow_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属工作流定义 id',
workflow_component_id BIGINT NOT NULL DEFAULT 0, `workflow_component_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '引用的组件 id',
user_id BIGINT NOT NULL DEFAULT 0, `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建人',
title VARCHAR(100) NOT NULL DEFAULT '', `title` varchar(100) NOT NULL DEFAULT '' COMMENT '节点标题',
remark VARCHAR(500) NOT NULL DEFAULT '', `remark` varchar(500) NOT NULL DEFAULT '' COMMENT '节点备注',
input_config JSON NOT NULL DEFAULT ('{}'), `input_config` json NOT NULL COMMENT '输入参数模板,例:{"params":[{"name":"user_define_param01","type":"string"}]}',
node_config JSON NOT NULL DEFAULT ('{}'), `node_config` json DEFAULT NULL COMMENT '节点执行配置,例:{"params":[{"prompt":"Summarize the following content:{user_define_param01}"}]}',
position_x DOUBLE NOT NULL DEFAULT 0, `position_x` double NOT NULL DEFAULT '0' COMMENT '画布 x 坐标',
position_y DOUBLE NOT NULL DEFAULT 0, `position_y` double NOT NULL DEFAULT '0' COMMENT '画布 y 坐标',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted TINYINT(1) NOT NULL DEFAULT 0, `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除0 正常1 已删',
INDEX idx_workflow_node_workflow_id (workflow_id) PRIMARY KEY (`id`),
) ENGINE = InnoDB KEY `idx_workflow_node_workflow_id` (`workflow_id`)
DEFAULT CHARSET = utf8mb4 ) ENGINE=InnoDB AUTO_INCREMENT=269 DEFAULT CHARSET=utf8mb4 COMMENT='工作流定义的节点 | Node of Workflow Definition';
COMMENT ='工作流定义的节点 | Node of Workflow Definition';
CREATE TABLE t_workflow_edge
CREATE TABLE `t_workflow_runtime_node`
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY, `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
uuid VARCHAR(32) NOT NULL DEFAULT '', `uuid` varchar(32) NOT NULL DEFAULT '' COMMENT '节点运行实例唯一标识',
workflow_id BIGINT NOT NULL DEFAULT 0, `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建人',
source_node_uuid VARCHAR(32) NOT NULL DEFAULT '', `workflow_runtime_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属运行实例 id',
source_handle VARCHAR(32) NOT NULL DEFAULT '', `node_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '对应工作流定义里的节点 id',
target_node_uuid VARCHAR(32) NOT NULL DEFAULT '', `input` json DEFAULT NULL COMMENT '节点本次输入数据',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `output` json DEFAULT NULL COMMENT '节点本次输出数据',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `status` smallint(6) NOT NULL DEFAULT '1' COMMENT '节点执行状态1 进行中2 失败3 成功',
is_deleted TINYINT(1) NOT NULL DEFAULT 0, `status_remark` varchar(250) NOT NULL DEFAULT '' COMMENT '状态补充说明,如失败堆栈',
INDEX idx_workflow_edge_workflow_id (workflow_id) `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
) ENGINE = InnoDB `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
DEFAULT CHARSET = utf8mb4; `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除0 正常1 已删',
PRIMARY KEY (`id`),
KEY `idx_runtime_node_runtime_id` (`workflow_runtime_id`),
KEY `idx_runtime_node_node_id` (`node_id`)
) ENGINE=InnoDB AUTO_INCREMENT=805 DEFAULT CHARSET=utf8mb4 COMMENT='工作流实例(运行时)- 节点 | Workflow Runtime Node';
CREATE TABLE t_workflow_runtime
CREATE TABLE `t_workflow_edge`
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY, `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
uuid VARCHAR(32) NOT NULL DEFAULT '', `uuid` varchar(32) NOT NULL DEFAULT '' COMMENT '边唯一标识',
user_id BIGINT NOT NULL DEFAULT 0, `workflow_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属工作流定义 id',
workflow_id BIGINT NOT NULL DEFAULT 0, `source_node_uuid` varchar(32) NOT NULL DEFAULT '' COMMENT '起始节点 uuid',
input JSON NOT NULL DEFAULT ('{}'), `source_handle` varchar(32) NOT NULL DEFAULT '' COMMENT '起始锚点标识',
output JSON NOT NULL DEFAULT ('{}'), `target_node_uuid` varchar(32) NOT NULL DEFAULT '' COMMENT '目标节点 uuid',
status SMALLINT NOT NULL DEFAULT 1 COMMENT '执行状态1就绪2执行中3成功4失败', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
status_remark VARCHAR(250) NOT NULL DEFAULT '' COMMENT '状态备注', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除0 正常1 已删',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`),
is_deleted TINYINT(1) NOT NULL DEFAULT 0, KEY `idx_workflow_edge_workflow_id` (`workflow_id`)
INDEX idx_workflow_runtime_workflow_id (workflow_id), ) ENGINE=InnoDB AUTO_INCREMENT=199 DEFAULT CHARSET=utf8mb4 COMMENT='工作流定义的边 | Edge of Workflow Definition';
INDEX idx_workflow_runtime_user_id (user_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COMMENT ='工作流实例(运行时)| Workflow Runtime';
CREATE TABLE t_workflow_runtime_node
CREATE TABLE `t_workflow_component`
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY, `id` bigint(20) NOT NULL AUTO_INCREMENT,
uuid VARCHAR(32) NOT NULL DEFAULT '', `uuid` varchar(32) NOT NULL DEFAULT '',
user_id BIGINT NOT NULL DEFAULT 0, `name` varchar(32) NOT NULL DEFAULT '',
workflow_runtime_id BIGINT NOT NULL DEFAULT 0, `title` varchar(100) NOT NULL DEFAULT '',
node_id BIGINT NOT NULL DEFAULT 0, `remark` text NOT NULL,
input JSON NOT NULL DEFAULT ('{}'), `display_order` int(11) NOT NULL DEFAULT '0',
output JSON NOT NULL DEFAULT ('{}'), `is_enable` tinyint(1) NOT NULL DEFAULT '0',
status SMALLINT NOT NULL DEFAULT 1 COMMENT '执行状态1进行中2失败3成功', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
status_remark VARCHAR(250) NOT NULL DEFAULT '' COMMENT '状态备注', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`),
is_deleted TINYINT(1) NOT NULL DEFAULT 0, KEY `idx_display_order` (`display_order`)
INDEX idx_runtime_node_runtime_id (workflow_runtime_id), ) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COMMENT='工作流组件库 | Workflow Component';
INDEX idx_runtime_node_node_id (node_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COMMENT ='工作流实例(运行时)- 节点 | Workflow Runtime Node';
CREATE TABLE t_workflow_component CREATE TABLE `t_workflow_runtime`
( (
id BIGINT AUTO_INCREMENT PRIMARY KEY, `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
uuid VARCHAR(32) DEFAULT '' NOT NULL, `uuid` varchar(32) NOT NULL DEFAULT '' COMMENT '运行实例唯一标识',
name VARCHAR(32) DEFAULT '' NOT NULL, `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '启动人',
title VARCHAR(100) DEFAULT '' NOT NULL, `workflow_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '对应工作流定义 id',
remark TEXT NOT NULL, `input` json DEFAULT NULL COMMENT '运行输入,例:{"userInput01":"text01","userInput02":true,"userInput03":10,"userInput04":["selectedA","selectedB"],"userInput05":["https://a.com/a.xlsx","https://a.com/b.png"]}',
display_order INT DEFAULT 0 NOT NULL, `output` json DEFAULT NULL COMMENT '运行输出,成功或失败的结果',
is_enable TINYINT(1) DEFAULT 0 NOT NULL, `status` smallint(6) NOT NULL DEFAULT '1' COMMENT '执行状态1 就绪2 执行中3 成功4 失败',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, `status_remark` varchar(250) NOT NULL DEFAULT '' COMMENT '状态补充说明,如失败原因',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
is_deleted TINYINT(1) DEFAULT 0 NOT NULL, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
INDEX idx_display_order (display_order) `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除0 正常1 已删',
) ENGINE = InnoDB PRIMARY KEY (`id`),
DEFAULT CHARSET = utf8mb4 KEY `idx_workflow_runtime_workflow_id` (`workflow_id`),
COMMENT '工作流组件库 | Workflow Component'; KEY `idx_workflow_runtime_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=297 DEFAULT CHARSET=utf8mb4 COMMENT='工作流实例(运行时)| Workflow Runtime';
-- workflow -- workflow
@@ -121,42 +120,17 @@ insert into t_workflow_component(uuid, name, title, remark, is_enable)
values (replace(uuid(), '-', ''), 'End', '结束', '流程由此结束', true); values (replace(uuid(), '-', ''), 'End', '结束', '流程由此结束', true);
insert into t_workflow_component(uuid, name, title, remark, is_enable) insert into t_workflow_component(uuid, name, title, remark, is_enable)
values (replace(uuid(), '-', ''), 'Answer', '生成回答', '调用大语言模型回答问题', true); values (replace(uuid(), '-', ''), 'Answer', '生成回答', '调用大语言模型回答问题', true);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'Dalle3', 'DALL-E 3 画图', '调用Dall-e-3生成图片', 11, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'DocumentExtractor', '文档提取', '从文档中提取信息', 4, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'KeywordExtractor', '关键词提取',
'从内容中提取关键词Top N指定需要提取的关键词数量', 5, false);
insert into t_workflow_component(uuid, name, title, remark, is_enable)
values (replace(uuid(), '-', ''), 'KnowledgeRetrieval', '知识检索', '从知识库中检索信息,需选中知识库',
true);
insert into t_workflow_component(uuid, name, title, remark, is_enable)
values (replace(uuid(), '-', ''), 'Switcher', '条件分支', '根据设置的条件引导执行不同的流程', false);
insert into t_workflow_component(uuid, name, title, remark, is_enable)
values (replace(uuid(), '-', ''), 'Classifier', '内容归类',
'使用大语言模型对输入信息进行分析并归类,根据类别调用对应的下游节点', false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'Template', '模板转换',
'将多个变量合并成一个输出内容', 10, true);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'Google', 'Google搜索', '从Google中检索信息', 13, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'FaqExtractor', '常见问题提取',
'从内容中提取出常见问题及对应的答案Top N为提取的数量',
6, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'Tongyiwanx', '通义万相-画图', '调用文生图模型生成图片', 12, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'HumanFeedback', '人机交互',
'中断执行中的流程并等待用户的输入,用户输入后继续执行后续流程', 10, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'MailSend', '邮件发送', '发送邮件到指定邮箱', 10, false);
insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable)
values (replace(uuid(), '-', ''), 'HttpRequest', 'Http请求',
'通过Http协议发送请求可将其他组件的输出作为参数也可设置常量作为参数。', 10, false);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1976160997656043521, '流程管理', 0, 1, 'flow', '', null, 1, 0, 'M', '0', '0', null, 'ph:user-fill', null, null, '2025-10-09 13:41:12', 1, '2025-10-20 20:59:25', '');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) VALUES (1976161221409579010, '工作流编排', 1976160997656043521, 0, 'workflow', 'workflow/index', null, 1, 0, 'C', '0', '0', null, 'ph:user-fill', null, null, '2025-10-09 13:42:05', 1, '2025-10-20 20:59:16', ''); INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by,
update_time, remark)
VALUES (1976160997656043521, '流程管理', 0, 1, 'flow', '', null, 1, 0, 'M', '0', '0', null, 'ph:user-fill', null, null,
'2025-10-09 13:41:12', 1, '2025-10-20 20:59:25', '');
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query_param, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by,
update_time, remark)
VALUES (1976161221409579010, '工作流编排', 1976160997656043521, 0, 'workflow', 'workflow/index', null, 1, 0, 'C', '0',
'0', null, 'ph:user-fill', null, null, '2025-10-09 13:42:05', 1, '2025-10-20 20:59:16', '');