Compare commits

...

55 Commits

Author SHA1 Message Date
YunaiV
7c94085499 2.4.2 版本发布 2025-04-12 12:19:56 +08:00
YunaiV
fdd15424fc 171 fix(protection): 修复HTTP接口签名 API重复请求问题 2025-04-12 12:01:00 +08:00
YunaiV
f6601ab639 171 fix(protection): 修复HTTP接口签名 API重复请求问题 2025-04-12 11:32:18 +08:00
YunaiV
4d88cfdf86 【代码修复】BPM:BpmProcessIdRedisDAO 的时间处理不对 2025-04-12 10:47:32 +08:00
YunaiV
42dbd4e21a 【代码评审】INFRA:代码生成在 vben5 的模版 2025-04-12 09:48:15 +08:00
芋道源码
f83d93bd5b !1321 【功能新增】INFRA: vben next schema 单表和树表代码生成
Merge pull request !1321 from puhui999/master-jdk17
2025-04-12 01:26:11 +00:00
YunaiV
5d77e3751b 【缺陷修复】AI:临时修复 tinyflow 在 groovy 冲突的问题 2025-04-12 09:22:00 +08:00
YunaiV
160771264f Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17 2025-04-12 08:58:22 +08:00
YunaiV
e56b99f2a7 Merge branch 'feature/ai' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17 2025-04-12 08:57:32 +08:00
芋道源码
e2f158996b !1318 排除TinyFlow的AI工作流中的agents-flex-store-elasticsearch, 在没用到es的时候一直检测es存活
Merge pull request !1318 from neviabit/N/A
2025-04-12 00:57:06 +00:00
YunaiV
911f5f8bf3 【代码评审】AI:PPT 相关 2025-04-12 08:54:35 +08:00
芋道源码
c357e7ae55 !1317 【优化代码】AI PPT:优化文多多、讯飞 API
Merge pull request !1317 from 小新/feature/ai
2025-04-12 00:48:47 +00:00
puhui999
37e5152a35 【代码优化】INFRA: vben next 树表代码生成 2025-04-11 21:38:39 +08:00
puhui999
015565cc9a 【功能新增】INFRA: vben next 树表代码生成 2025-04-11 18:21:42 +08:00
puhui999
69486939d5 【功能新增】INFRA: vben next 单表代码生成 2025-04-11 16:47:11 +08:00
xiaoxin
47df4bb21f feat: mcp demo 2025-04-09 13:16:29 +08:00
neviabit
8b49e5a94d 排除TinyFlow的AI工作流中的agents-flex-store-elasticsearch, 防止一直检测失败导致的Elasticsearch health check failed, 这导致了日志logs被这些无意义的异常给占满
Signed-off-by: neviabit <10192451+neviabit@user.noreply.gitee.com>
2025-04-08 09:20:56 +00:00
xiaoxin
3445baf926 Merge remote-tracking branch 'yd_origin/feature/ai' into feature/ai 2025-04-07 15:45:59 +08:00
YunaiV
be416b7d78 【代码优化】MALL:在线客服的注释 2025-04-03 23:33:21 +08:00
YunaiV
1e4e02ec2f Merge branch 'develop' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17 2025-04-01 19:16:02 +08:00
芋道源码
a484007d97 !1312 修复了一些问题
Merge pull request !1312 from puhui999/develop-tmp
2025-04-01 11:15:55 +00:00
puhui999
40b6e5a3bb 【缺陷修复】商城:检查是否包邮 2025-04-01 18:04:17 +08:00
puhui999
81124292fd 【代码优化】商城:客服消息携带用户头像信息 2025-04-01 16:28:02 +08:00
YunaiV
fd2d067324 Merge branch 'feature/ai' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17 2025-03-30 10:53:39 +08:00
YunaiV
a53c7dafd3 Merge branch 'feature/bpm' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17 2025-03-30 10:53:17 +08:00
芋道源码
7179786a45 !1305 feat: AI工作流优化
Merge pull request !1305 from Lesan/feature/ai-工作流
2025-03-30 02:19:03 +00:00
jason
d7a785d1de 【问题修复】 并行网关保存问题 2025-03-28 21:41:40 +08:00
Lesan
2aff972600 feat: AI工作流优化 2025-03-28 15:54:46 +08:00
芋道源码
743d88a4fc !1301 条件判断添加支持JOIN表的属性
Merge pull request !1301 from QINY/master-jdk17
2025-03-25 13:09:01 +00:00
YunaiV
f0c26d5b5a 【代码评审】BPM:子流程的代码 2025-03-25 12:49:34 +08:00
芋道源码
2cf5e17f4b !1303 子流程优化
Merge pull request !1303 from Lesan/feature/bpm-子流程
2025-03-25 04:44:01 +00:00
Lesan
c9a8548920 feat: 流程时间线适配子流程节点 2025-03-25 11:07:24 +08:00
Lesan
b471dc55c3 fix: 存在子流程情况下的取消逻辑优化 2025-03-25 10:37:38 +08:00
Lesan
e14716a307 fix: 存在子流程情况下的取消逻辑优化 2025-03-25 10:24:34 +08:00
YunaiV
4bb52bb37d 【代码评审】AI:工作流 2025-03-25 09:55:05 +08:00
芋道源码
c8e63f4a8c !1300 feat: AI工作流
Merge pull request !1300 from Lesan/feature/ai-工作流
2025-03-25 01:21:55 +00:00
qiny
ba4d4540ab 条件判断添加支持JOIN表的属性 2025-03-24 16:58:42 +08:00
Lesan
587504c36a feat: AI工作流 2025-03-24 15:15:27 +08:00
YunaiV
138239324c 【功能新增】AI:百川模型的接入 2025-03-23 12:48:13 +08:00
YunaiV
51d054c586 Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/ai 2025-03-23 12:18:44 +08:00
YunaiV
cd4813f7dd 【功能新增】AI:百川模型的接入 2025-03-23 12:18:37 +08:00
YunaiV
ef5e56d560 【代码优化】AI:硅基流动的图片生成 2025-03-23 11:22:46 +08:00
YunaiV
59c744520a 【代码优化】AI:硅基流动的图片生成 2025-03-23 10:41:51 +08:00
YunaiV
813e7af846 【代码评审】AI:硅基流动的图片生成 2025-03-23 10:27:01 +08:00
芋道源码
5dc93cb872 !1294 【功能新增】AI:画图通用功能增加硅基流动平台
Merge pull request !1294 from 拽拽的哥/feature/ai
2025-03-23 01:41:53 +00:00
xiaoxin
b2ec4d38a0 【优化代码】AI PPT:优化文多多、讯飞 API 2025-03-21 14:41:02 +08:00
zzt
ecf4df8620 【功能新增】AI:腾讯图像创作 2025-03-20 08:04:32 +08:00
xiaoxin
270fea0c5d 【解决 TODO 】AI PPT:解决一些 TODO 2025-03-18 14:41:06 +08:00
zzt
7b3401e216 【功能新增】AI:画图通用功能增加硅基流动平台 2025-03-18 00:22:15 +08:00
YunaiV
acf68b1cec 【代码评审】AI:PPT API 的接入 2025-03-17 22:15:57 +08:00
芋道源码
1db3b867aa !1293 【功能新增】AI:讯飞、文多多 PPT API 对接
Merge pull request !1293 from 小新/feature/ai
2025-03-17 13:52:02 +00:00
xiaoxin
ecc3bd281c 【功能新增】AI:讯飞 PPT API 对接,测试用例完善 2025-03-17 15:44:17 +08:00
xiaoxin
c79273c5d6 【测试用例新增】AI:文多多 API 测试完善 2025-03-17 09:46:47 +08:00
xiaoxin
a82abed2b5 【功能新增】AI:对接文多多 v2 接口 2025-03-15 22:38:09 +08:00
xiaoxin
ea1d9f0075 【功能新增】AI:文多多对接 2025-03-09 21:43:45 +08:00
60 changed files with 3681 additions and 138 deletions

View File

@@ -32,7 +32,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2.4.1-SNAPSHOT</revision>
<revision>2.4.2-SNAPSHOT</revision>
<!-- Maven 相关 -->
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

View File

@@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2.4.1-SNAPSHOT</revision>
<revision>2.4.2-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 -->
<spring.boot.version>3.4.1</spring.boot.version>

View File

@@ -20,7 +20,7 @@ import java.util.function.Consumer;
*/
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
public MPJLambdaWrapperX<T> likeIfPresent(SFunction<T, ?> column, String val) {
public <R> MPJLambdaWrapperX<T> likeIfPresent(SFunction<R, ?> column, String val) {
MPJWrappers.lambdaJoin().like(column, val);
if (StringUtils.hasText(val)) {
return (MPJLambdaWrapperX<T>) super.like(column, val);
@@ -28,63 +28,63 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
public <R> MPJLambdaWrapperX<T> inIfPresent(SFunction<R, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
}
return this;
}
public MPJLambdaWrapperX<T> inIfPresent(SFunction<T, ?> column, Object... values) {
public <R> MPJLambdaWrapperX<T> inIfPresent(SFunction<R, ?> column, Object... values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values);
}
return this;
}
public MPJLambdaWrapperX<T> eqIfPresent(SFunction<T, ?> column, Object val) {
public <R> MPJLambdaWrapperX<T> eqIfPresent(SFunction<R, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (MPJLambdaWrapperX<T>) super.eq(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> neIfPresent(SFunction<T, ?> column, Object val) {
public <R> MPJLambdaWrapperX<T> neIfPresent(SFunction<R, ?> column, Object val) {
if (ObjectUtil.isNotEmpty(val)) {
return (MPJLambdaWrapperX<T>) super.ne(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> gtIfPresent(SFunction<T, ?> column, Object val) {
public <R> MPJLambdaWrapperX<T> gtIfPresent(SFunction<R, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.gt(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> geIfPresent(SFunction<T, ?> column, Object val) {
public <R> MPJLambdaWrapperX<T> geIfPresent(SFunction<R, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.ge(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> ltIfPresent(SFunction<T, ?> column, Object val) {
public <R> MPJLambdaWrapperX<T> ltIfPresent(SFunction<R, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.lt(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> leIfPresent(SFunction<T, ?> column, Object val) {
public <R> MPJLambdaWrapperX<T> leIfPresent(SFunction<R, ?> column, Object val) {
if (val != null) {
return (MPJLambdaWrapperX<T>) super.le(column, val);
}
return this;
}
public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object val1, Object val2) {
public <R> MPJLambdaWrapperX<T> betweenIfPresent(SFunction<R, ?> column, Object val1, Object val2) {
if (val1 != null && val2 != null) {
return (MPJLambdaWrapperX<T>) super.between(column, val1, val2);
}
@@ -97,7 +97,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
public MPJLambdaWrapperX<T> betweenIfPresent(SFunction<T, ?> column, Object[] values) {
public <R> MPJLambdaWrapperX<T> betweenIfPresent(SFunction<R, ?> column, Object[] values) {
Object val1 = ArrayUtils.get(values, 0);
Object val2 = ArrayUtils.get(values, 1);
return betweenIfPresent(column, val1, val2);
@@ -310,4 +310,4 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this;
}
}
}

View File

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.framework.signature.core.aop;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
@@ -69,13 +71,17 @@ public class ApiSignatureAspect {
// 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2
String nonce = request.getHeader(signature.nonce());
signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit());
if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) {
String timestamp = request.getHeader(signature.timestamp());
log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature);
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求");
}
return true;
}
/**
* 校验请求头加签参数
*
* <p>
* 1. appId 是否为空
* 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
* 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
@@ -118,7 +124,7 @@ public class ApiSignatureAspect {
/**
* 构建签名字符串
*
* <p>
* 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
*
* @param signature signature
@@ -139,7 +145,7 @@ public class ApiSignatureAspect {
/**
* 获取请求头加签参数 Map
*
* @param request 请求
* @param request 请求
* @param signature 签名注解
* @return signature params
*/

View File

@@ -17,7 +17,7 @@ public class ApiSignatureRedisDAO {
/**
* 验签随机数
*
* <p>
* KEY 格式signature_nonce:%s // 参数为 随机数
* VALUE 格式String
* 过期时间:不固定
@@ -26,7 +26,7 @@ public class ApiSignatureRedisDAO {
/**
* 签名密钥
*
* <p>
* HASH 结构
* KEY 格式:%s // 参数为 appid
* VALUE 格式String
@@ -40,8 +40,8 @@ public class ApiSignatureRedisDAO {
return stringRedisTemplate.opsForValue().get(formatNonceKey(appId, nonce));
}
public void setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(formatNonceKey(appId, nonce), "", time, timeUnit);
public Boolean setNonce(String appId, String nonce, int time, TimeUnit timeUnit) {
return stringRedisTemplate.opsForValue().setIfAbsent(formatNonceKey(appId, nonce), "", time, timeUnit);
}
private static String formatNonceKey(String appId, String nonce) {

View File

@@ -63,13 +63,12 @@ public class ApiSignatureTest {
when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
// mock 方法
when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
when(signatureRedisDAO.setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS))).thenReturn(true);
// 调用
boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
// 断言结果
assertTrue(result);
// 断言调用
verify(signatureRedisDAO).setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS));
}
}

View File

@@ -61,4 +61,8 @@ public interface ErrorCodeConstants {
ErrorCode TOOL_NOT_EXISTS = new ErrorCode(1_040_010_000, "工具不存在");
ErrorCode TOOL_NAME_NOT_EXISTS = new ErrorCode(1_040_010_001, "工具({})找不到 Bean");
// ========== AI 工作流 1-040-011-000 ==========
ErrorCode WORKFLOW_NOT_EXISTS = new ErrorCode(1_040_011_000, "工作流不存在");
ErrorCode WORKFLOW_CODE_EXISTS = new ErrorCode(1_040_011_001, "工作流标识已存在");
}

View File

@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.ai.controller.admin.workflow;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.*;
import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO;
import cn.iocoder.yudao.module.ai.service.workflow.AiWorkflowService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - AI 工作流")
@RestController
@RequestMapping("/ai/workflow")
@Slf4j
public class AiWorkflowController {
@Resource
private AiWorkflowService workflowService;
@PostMapping("/create")
@Operation(summary = "创建 AI 工作流")
@PreAuthorize("@ss.hasPermission('ai:workflow:create')")
public CommonResult<Long> createWorkflow(@Valid @RequestBody AiWorkflowSaveReqVO createReqVO) {
return success(workflowService.createWorkflow(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新 AI 工作流")
@PreAuthorize("@ss.hasPermission('ai:workflow:update')")
public CommonResult<Boolean> updateWorkflow(@Valid @RequestBody AiWorkflowSaveReqVO updateReqVO) {
workflowService.updateWorkflow(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除 AI 工作流")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('ai:workflow:delete')")
public CommonResult<Boolean> deleteWorkflow(@RequestParam("id") Long id) {
workflowService.deleteWorkflow(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得 AI 工作流")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:workflow:query')")
public CommonResult<AiWorkflowRespVO> getWorkflow(@RequestParam("id") Long id) {
AiWorkflowDO workflow = workflowService.getWorkflow(id);
return success(BeanUtils.toBean(workflow, AiWorkflowRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得 AI 工作流分页")
@PreAuthorize("@ss.hasPermission('ai:workflow:query')")
public CommonResult<PageResult<AiWorkflowRespVO>> getWorkflowPage(@Valid AiWorkflowPageReqVO pageReqVO) {
PageResult<AiWorkflowDO> pageResult = workflowService.getWorkflowPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiWorkflowRespVO.class));
}
@PostMapping("/test")
@Operation(summary = "测试 AI 工作流")
@PreAuthorize("@ss.hasPermission('ai:workflow:test')")
public CommonResult<Object> testWorkflow(@Valid @RequestBody AiWorkflowTestReqVO testReqVO) {
return success(workflowService.testWorkflow(testReqVO));
}
}

View File

@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - AI 工作流分页 Request VO")
@Data
public class AiWorkflowPageReqVO extends PageParam {
@Schema(description = "名称", example = "工作流")
private String name;
@Schema(description = "标识", example = "FLOW")
private String code;
@Schema(description = "状态", example = "1")
@InEnum(CommonStatusEnum.class)
private Integer status;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI 工作流 Response VO")
@Data
public class AiWorkflowRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "工作流标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW")
private String code;
@Schema(description = "工作流名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流")
private String name;
@Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流")
private String remark;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status;
@Schema(description = "工作流模型 JSON", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private String graph;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式")
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI 工作流新增/修改 Request VO")
@Data
public class AiWorkflowSaveReqVO {
@Schema(description = "编号", example = "1")
private Long id;
@Schema(description = "工作流标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW")
@NotEmpty(message = "工作流标识不能为空")
private String code;
@Schema(description = "工作流名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流")
@NotEmpty(message = "工作流名称不能为空")
private String name;
@Schema(description = "备注", example = "FLOW")
private String remark;
@Schema(description = "工作流模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
@NotEmpty(message = "工作流模型不能为空")
private String graph;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW")
@NotNull(message = "状态不能为空")
private Integer status;
}

View File

@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.Map;
@Schema(description = "管理后台 - AI 工作流测试 Request VO")
@Data
public class AiWorkflowTestReqVO {
@Schema(description = "工作流模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
@NotEmpty(message = "工作流模型不能为空")
private String graph;
@Schema(description = "参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private Map<String, Object> params;
}

View File

@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.workflow;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* AI 工作流 DO
*
* @author lesan
*/
@TableName(value = "ai_workflow", autoResultMap = true)
@KeySequence("ai_workflow") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
public class AiWorkflowDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 工作流名称
*/
private String name;
/**
* 工作流标识
*/
private String code;
/**
* 工作流模型 JSON 数据
*/
private String graph;
/**
* 备注
*/
private String remark;
/**
* 状态
*
* 枚举 {@link CommonStatusEnum}
*/
private Integer status;
}

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.ai.dal.mysql.workflow;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO;
import org.apache.ibatis.annotations.Mapper;
/**
* AI 工作流 Mapper
*
* @author lesan
*/
@Mapper
public interface AiWorkflowMapper extends BaseMapperX<AiWorkflowDO> {
default AiWorkflowDO selectByCode(String code) {
return selectOne(AiWorkflowDO::getCode, code);
}
default PageResult<AiWorkflowDO> selectPage(AiWorkflowPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiWorkflowDO>()
.eqIfPresent(AiWorkflowDO::getStatus, pageReqVO.getStatus())
.likeIfPresent(AiWorkflowDO::getName, pageReqVO.getName())
.likeIfPresent(AiWorkflowDO::getCode, pageReqVO.getCode())
.betweenIfPresent(AiWorkflowDO::getCreateTime, pageReqVO.getCreateTime()));
}
}

View File

@@ -11,6 +11,7 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
@@ -144,7 +145,12 @@ public class AiImageServiceImpl implements AiImageService {
.withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格
.withResponseFormat("b64_json")
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) {
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) {
// https://docs.siliconflow.cn/cn/api-reference/images/images-generations
return SiliconFlowImageOptions.builder().model(model.getModel())
.height(draw.getHeight()).width(draw.getWidth())
.build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) {
// https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
// https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage
return StabilityAiImageOptions.builder().model(model.getModel())

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.ai.service.workflow;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowSaveReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO;
import jakarta.validation.Valid;
/**
* AI 工作流 Service 接口
*
* @author lesan
*/
public interface AiWorkflowService {
/**
* 创建 AI 工作流
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createWorkflow(@Valid AiWorkflowSaveReqVO createReqVO);
/**
* 更新 AI 工作流
*
* @param updateReqVO 更新信息
*/
void updateWorkflow(@Valid AiWorkflowSaveReqVO updateReqVO);
/**
* 删除 AI 工作流
*
* @param id 编号
*/
void deleteWorkflow(Long id);
/**
* 获得 AI 工作流
*
* @param id 编号
* @return AI 工作流
*/
AiWorkflowDO getWorkflow(Long id);
/**
* 获得 AI 工作流分页
*
* @param pageReqVO 分页查询
* @return AI 工作流分页
*/
PageResult<AiWorkflowDO> getWorkflowPage(AiWorkflowPageReqVO pageReqVO);
/**
* 测试 AI 工作流
*
* @param testReqVO 测试数据
*/
Object testWorkflow(AiWorkflowTestReqVO testReqVO);
}

View File

@@ -0,0 +1,150 @@
package cn.iocoder.yudao.module.ai.service.workflow;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowSaveReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO;
import cn.iocoder.yudao.module.ai.dal.mysql.workflow.AiWorkflowMapper;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import dev.tinyflow.core.Tinyflow;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WORKFLOW_CODE_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WORKFLOW_NOT_EXISTS;
/**
* AI 工作流 Service 实现类
*
* @author lesan
*/
@Service
@Slf4j
public class AiWorkflowServiceImpl implements AiWorkflowService {
@Resource
private AiWorkflowMapper workflowMapper;
@Resource
private AiApiKeyService apiKeyService;
@Override
public Long createWorkflow(AiWorkflowSaveReqVO createReqVO) {
validateWorkflowForCreateOrUpdate(null, createReqVO.getCode());
AiWorkflowDO workflow = BeanUtils.toBean(createReqVO, AiWorkflowDO.class);
workflowMapper.insert(workflow);
return workflow.getId();
}
@Override
public void updateWorkflow(AiWorkflowSaveReqVO updateReqVO) {
validateWorkflowForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getCode());
AiWorkflowDO workflow = BeanUtils.toBean(updateReqVO, AiWorkflowDO.class);
workflowMapper.updateById(workflow);
}
@Override
public void deleteWorkflow(Long id) {
validateWorkflowExists(id);
workflowMapper.deleteById(id);
}
@Override
public AiWorkflowDO getWorkflow(Long id) {
return workflowMapper.selectById(id);
}
@Override
public PageResult<AiWorkflowDO> getWorkflowPage(AiWorkflowPageReqVO pageReqVO) {
return workflowMapper.selectPage(pageReqVO);
}
@Override
public Object testWorkflow(AiWorkflowTestReqVO testReqVO) {
Map<String, Object> variables = testReqVO.getParams();
Tinyflow tinyflow = parseFlowParam(testReqVO.getGraph());
return tinyflow.toChain().executeForResult(variables);
}
private void validateWorkflowForCreateOrUpdate(Long id, String code) {
validateWorkflowExists(id);
validateCodeUnique(id, code);
}
private void validateWorkflowExists(Long id) {
if (ObjUtil.isNull(id)) {
return;
}
AiWorkflowDO workflow = workflowMapper.selectById(id);
if (ObjUtil.isNull(workflow)) {
throw exception(WORKFLOW_NOT_EXISTS);
}
}
private void validateCodeUnique(Long id, String code) {
if (StrUtil.isBlank(code)) {
return;
}
AiWorkflowDO workflow = workflowMapper.selectByCode(code);
if (ObjUtil.isNull(workflow)) {
return;
}
if (ObjUtil.isNull(id)) {
throw exception(WORKFLOW_CODE_EXISTS);
}
if (ObjUtil.notEqual(workflow.getId(), id)) {
throw exception(WORKFLOW_CODE_EXISTS);
}
}
private Tinyflow parseFlowParam(String graph) {
// TODO @lesan可以使用 jackson 哇?
JSONObject json = JSONObject.parseObject(graph);
JSONArray nodeArr = json.getJSONArray("nodes");
Tinyflow tinyflow = new Tinyflow(json.toJSONString());
for (int i = 0; i < nodeArr.size(); i++) {
JSONObject node = nodeArr.getJSONObject(i);
switch (node.getString("type")) {
case "llmNode":
JSONObject data = node.getJSONObject("data");
AiApiKeyDO apiKey = apiKeyService.getApiKey(data.getLong("llmId"));
switch (apiKey.getPlatform()) {
// TODO @lesan 需要讨论一下这里怎么弄
// TODO @lesan llmId 对应 model 的编号如何?这样的话,就是 apiModelService 提供一个获取 LLM 的方法。然后,创建的方法,也在 AiModelFactory 提供。可以先接个 deepseek 先。deepseek yyds
case "OpenAI":
break;
case "Ollama":
break;
case "YiYan":
break;
case "XingHuo":
break;
case "TongYi":
break;
case "DeepSeek":
break;
case "ZhiPu":
break;
}
break;
case "internalNode":
break;
default:
break;
}
}
return tinyflow;
}
}

View File

@@ -15,6 +15,7 @@
<description>AI 大模型拓展,接入国内外大模型</description>
<properties>
<spring-ai.version>1.0.0-M6</spring-ai.version>
<tinyflow.version>1.0.0-rc.3</tinyflow.version>
</properties>
<dependencies>
@@ -117,6 +118,25 @@
</exclusions>
</dependency>
<!-- TinyFlowAI 工作流 -->
<dependency>
<groupId>dev.tinyflow</groupId>
<artifactId>tinyflow-java-core</artifactId>
<version>${tinyflow.version}</version>
<exclusions>
<exclusion>
<!-- 解决 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1318/ 问题 -->
<groupId>com.agentsflex</groupId>
<artifactId>agents-flex-store-elasticsearch</artifactId>
</exclusion>
<exclusion>
<!-- TODO @芋艿:暂时移除 groovy和 iot 冲突 -->
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -4,10 +4,12 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
@@ -113,11 +115,11 @@ public class YudaoAiAutoConfiguration {
public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(SiliconFlowChatModel.MODEL_DEFAULT);
properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(SiliconFlowChatModel.BASE_URL)
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
@@ -192,6 +194,33 @@ public class YudaoAiAutoConfiguration {
return new XingHuoChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan();
return buildBaiChuanChatClient(properties);
}
public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new BaiChuanChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {

View File

@@ -43,6 +43,12 @@ public class YudaoAiProperties {
@SuppressWarnings("SpellCheckingInspection")
private XingHuoProperties xinghuo;
/**
* 百川
*/
@SuppressWarnings("SpellCheckingInspection")
private BaiChuanProperties baichuan;
/**
* Midjourney 绘图
*/
@@ -122,6 +128,19 @@ public class YudaoAiProperties {
}
@Data
public static class BaiChuanProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class MidjourneyProperties {

View File

@@ -27,6 +27,7 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
SILICON_FLOW("SiliconFlow", "硅基流动"), // 硅基流动
MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技
MOONSHOT("Moonshot", "月之暗灭"), // KIMI
BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能
// ========== 国外平台 ==========

View File

@@ -11,11 +11,15 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
@@ -42,6 +46,7 @@ import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration;
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration;
@@ -146,6 +151,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return buildMoonshotChatModel(apiKey, url);
case XING_HUO:
return buildXingHuoChatModel(apiKey);
case BAI_CHUAN:
return buildBaiChuanChatModel(apiKey);
case OPENAI:
return buildOpenAiChatModel(apiKey, url);
case AZURE_OPENAI:
@@ -182,6 +189,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(MoonshotChatModel.class);
case XING_HUO:
return SpringUtil.getBean(XingHuoChatModel.class);
case BAI_CHUAN:
return SpringUtil.getBean(AzureOpenAiChatModel.class);
case OPENAI:
return SpringUtil.getBean(OpenAiChatModel.class);
case AZURE_OPENAI:
@@ -203,6 +212,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(QianFanImageModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiImageModel.class);
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowImageModel.class);
case OPENAI:
return SpringUtil.getBean(OpenAiImageModel.class);
case STABLE_DIFFUSION:
@@ -224,6 +235,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return buildZhiPuAiImageModel(apiKey, url);
case OPENAI:
return buildOpenAiImageModel(apiKey, url);
case SILICON_FLOW:
return buildSiliconFlowImageModel(apiKey,url);
case STABLE_DIFFUSION:
return buildStabilityAiImageModel(apiKey, url);
default:
@@ -433,6 +446,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new YudaoAiAutoConfiguration().buildXingHuoChatClient(properties);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
*/
private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties()
.setApiKey(apiKey);
return new YudaoAiAutoConfiguration().buildBaiChuanChatClient(properties);
}
/**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法
*/
@@ -468,6 +490,15 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new OpenAiImageModel(openAiApi);
}
/**
* 创建 SiliconFlowImageModel 对象
*/
private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) {
url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL);
SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken);
return new SiliconFlowImageModel(openAiApi);
}
/**
* 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法
*/
@@ -476,6 +507,9 @@ public class AiModelFactoryImpl implements AiModelFactory {
return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build();
}
/**
* 可参考 {@link StabilityAiImageAutoConfiguration} 的 stabilityAiImageModel 方法
*/
private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) {
url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL);
StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url);

View File

@@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.ai.core.model.baichuan;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 百川 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class BaiChuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.baichuan-ai.com";
public static final String MODEL_DEFAULT = "Baichuan4-Turbo";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
/**
* SiliconFlow API 枚举类
*
* @author zzt
*/
public final class SiliconFlowApiConstants {
public static final String DEFAULT_BASE_URL = "https://api.siliconflow.cn";
public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B";
public static final String DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors";
public static final String PROVIDER_NAME = "Siiconflow";
}

View File

@@ -20,10 +20,6 @@ import reactor.core.publisher.Flux;
@RequiredArgsConstructor
public class SiliconFlowChatModel implements ChatModel {
public static final String BASE_URL = "https://api.siliconflow.cn";
public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B";
/**
* 兼容 OpenAI 接口,进行复用
*/

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import java.util.Map;
/**
* 硅基流动 Image API
*
* @see <a href= "https://docs.siliconflow.cn/cn/api-reference/images/images-generations">Images</a>
*
* @author zzt
*/
public class SiliconFlowImageApi {
private final RestClient restClient;
public SiliconFlowImageApi(String aiToken) {
this(SiliconFlowApiConstants.DEFAULT_BASE_URL, aiToken, RestClient.builder());
}
public SiliconFlowImageApi(String baseUrl, String openAiToken) {
this(baseUrl, openAiToken, RestClient.builder());
}
public SiliconFlowImageApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder) {
this(baseUrl, openAiToken, restClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
}
public SiliconFlowImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, responseErrorHandler);
}
public SiliconFlowImageApi(String baseUrl, String apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, responseErrorHandler);
}
public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// @formatter:off
this.restClient = restClientBuilder.baseUrl(baseUrl)
.defaultHeaders(h -> {
if(!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
})
.defaultStatusHandler(responseErrorHandler)
.build();
// @formatter:on
}
public ResponseEntity<OpenAiImageApi.OpenAiImageResponse> createImage(SiliconflowImageRequest siliconflowImageRequest) {
Assert.notNull(siliconflowImageRequest, "Image request cannot be null.");
Assert.hasLength(siliconflowImageRequest.prompt(), "Prompt cannot be empty.");
return this.restClient.post()
.uri("v1/images/generations")
.body(siliconflowImageRequest)
.retrieve()
.toEntity(OpenAiImageApi.OpenAiImageResponse.class);
}
// @formatter:off
@JsonInclude(JsonInclude.Include.NON_NULL)
public record SiliconflowImageRequest (
@JsonProperty("prompt") String prompt,
@JsonProperty("model") String model,
@JsonProperty("batch_size") Integer batchSize,
@JsonProperty("negative_prompt") String negativePrompt,
@JsonProperty("seed") Integer seed,
@JsonProperty("num_inference_steps") Integer numInferenceSteps,
@JsonProperty("guidance_scale") Float guidanceScale,
@JsonProperty("image") String image) {
public SiliconflowImageRequest(String prompt, String model) {
this(prompt, model, null, null, null, null, null, null);
}
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import io.micrometer.observation.ObservationRegistry;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.image.*;
import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationContext;
import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import java.util.List;
/**
* 硅基流动 {@link ImageModel} 实现类
*
* 参考 {@link OpenAiImageModel} 实现
*
* @author zzt
*/
public class SiliconFlowImageModel implements ImageModel {
private static final Logger logger = LoggerFactory.getLogger(SiliconFlowImageModel.class);
private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention();
private final SiliconFlowImageOptions defaultOptions;
private final RetryTemplate retryTemplate;
private final SiliconFlowImageApi siliconFlowImageApi;
private final ObservationRegistry observationRegistry;
@Setter
private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi) {
this(siliconFlowImageApi, SiliconFlowImageOptions.builder().build(), RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate) {
this(siliconFlowImageApi, options, retryTemplate, ObservationRegistry.NOOP);
}
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
Assert.notNull(siliconFlowImageApi, "OpenAiImageApi must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(retryTemplate, "retryTemplate must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.siliconFlowImageApi = siliconFlowImageApi;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
}
@Override
public ImageResponse call(ImagePrompt imagePrompt) {
SiliconFlowImageOptions requestImageOptions = mergeOptions(imagePrompt.getOptions(), this.defaultOptions);
SiliconFlowImageApi.SiliconflowImageRequest imageRequest = createRequest(imagePrompt, requestImageOptions);
var observationContext = ImageModelObservationContext.builder()
.imagePrompt(imagePrompt)
.provider(SiliconFlowApiConstants.PROVIDER_NAME)
.requestOptions(imagePrompt.getOptions())
.build();
return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity = this.retryTemplate
.execute(ctx -> this.siliconFlowImageApi.createImage(imageRequest));
ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest);
observationContext.setResponse(imageResponse);
return imageResponse;
});
}
private SiliconFlowImageApi.SiliconflowImageRequest createRequest(ImagePrompt imagePrompt,
SiliconFlowImageOptions requestImageOptions) {
String instructions = imagePrompt.getInstructions().get(0).getText();
SiliconFlowImageApi.SiliconflowImageRequest imageRequest = new SiliconFlowImageApi.SiliconflowImageRequest(instructions,
SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL);
return ModelOptionsUtils.merge(requestImageOptions, imageRequest, SiliconFlowImageApi.SiliconflowImageRequest.class);
}
private ImageResponse convertResponse(ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity,
SiliconFlowImageApi.SiliconflowImageRequest siliconflowImageRequest) {
OpenAiImageApi.OpenAiImageResponse imageApiResponse = imageResponseEntity.getBody();
if (imageApiResponse == null) {
logger.warn("No image response returned for request: {}", siliconflowImageRequest);
return new ImageResponse(List.of());
}
List<ImageGeneration> imageGenerationList = imageApiResponse.data()
.stream()
.map(entry -> new ImageGeneration(new Image(entry.url(), entry.b64Json()),
new OpenAiImageGenerationMetadata(entry.revisedPrompt())))
.toList();
ImageResponseMetadata openAiImageResponseMetadata = new ImageResponseMetadata(imageApiResponse.created());
return new ImageResponse(imageGenerationList, openAiImageResponseMetadata);
}
private SiliconFlowImageOptions mergeOptions(@Nullable ImageOptions runtimeOptions, SiliconFlowImageOptions defaultOptions) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class,
SiliconFlowImageOptions.class);
if (runtimeOptionsForProvider == null) {
return defaultOptions;
}
return SiliconFlowImageOptions.builder()
// Handle portable image options
.model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.batchSize(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getN(), defaultOptions.getN()))
.width(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getWidth(), defaultOptions.getWidth()))
.height(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getHeight(), defaultOptions.getHeight()))
// Handle SiliconFlow specific image options
.negativePrompt(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getNegativePrompt(), defaultOptions.getNegativePrompt()))
.numInferenceSteps(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getNumInferenceSteps(), defaultOptions.getNumInferenceSteps()))
.guidanceScale(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getGuidanceScale(), defaultOptions.getGuidanceScale()))
.seed(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getSeed(), defaultOptions.getSeed()))
.build();
}
}

View File

@@ -0,0 +1,105 @@
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.image.ImageOptions;
/**
* 硅基流动 {@link ImageOptions}
*
* @author zzt
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SiliconFlowImageOptions implements ImageOptions {
@JsonProperty("model")
private String model;
@JsonProperty("negative_prompt")
private String negativePrompt;
/**
* The number of images to generate. Must be between 1 and 4.
*/
@JsonProperty("image_size")
private String imageSize;
/**
* The number of images to generate. Must be between 1 and 4.
*/
@JsonProperty("batch_size")
private Integer batchSize = 1;
/**
* number of inference steps
*/
@JsonProperty("num_inference_steps")
private Integer numInferenceSteps = 25;
/**
* This value is used to control the degree of match between the generated image and the given prompt. The higher the value, the more the generated image will tend to strictly match the text prompt. The lower the value, the more creative and diverse the generated image will be, potentially containing more unexpected elements.
*
* Required range: 0 <= x <= 20
*/
@JsonProperty("guidance_scale")
private Float guidanceScale = 0.75F;
/**
* 如果想要每次都生成固定的图片,可以把 seed 设置为固定值
*
*/
@JsonProperty("seed")
private Integer seed = (int)(Math.random() * 1_000_000_000);
/**
* The image that needs to be uploaded should be converted into base64 format.
*/
@JsonProperty("image")
private String image;
/**
* 宽
*/
private Integer width;
/**
* 高
*/
private Integer height;
public void setHeight(Integer height) {
this.height = height;
if (this.width != null && this.height != null) {
this.imageSize = this.width + "x" + this.height;
}
}
public void setWidth(Integer width) {
this.width = width;
if (this.width != null && this.height != null) {
this.imageSize = this.width + "x" + this.height;
}
}
@Override
public Integer getN() {
return batchSize;
}
@Override
public String getResponseFormat() {
return "url";
}
@Override
public String getStyle() {
return null;
}
}

View File

@@ -0,0 +1,381 @@
package cn.iocoder.yudao.framework.ai.core.model.wenduoduo.api;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 文多多 API
*
* @author xiaoxin
* @see <a href="https://docmee.cn/open-platform/api">PPT 生成 API</a>
*/
@Slf4j
public class WenDuoDuoPptApi {
public static final String BASE_URL = "https://docmee.cn";
public static final String TOKEN_NAME = "token";
private final WebClient webClient;
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
HttpRequest request = response.request();
log.error("[WenDuoDuoPptApi] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]",
request.getMethod(), request.getURI(), reqParam, responseBody);
sink.error(new IllegalStateException("[WenDuoDuoPptApi] 调用失败!"));
});
public WenDuoDuoPptApi(String token) {
Assert.hasText(token, "token 不能为空");
this.webClient = WebClient.builder()
.baseUrl(BASE_URL)
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(TOKEN_NAME, token);
})
.build();
}
/**
* 创建 token
*
* @param request 请求信息
* @return token
*/
public String createApiToken(CreateTokenRequest request) {
return this.webClient.post()
.uri("/api/user/createApiToken")
.header("Api-Key", request.apiKey)
.body(Mono.just(request), CreateTokenRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(ApiResponse.class)
.<String>handle((response, sink) -> {
if (response.code != 0) {
sink.error(new IllegalStateException("创建 token 异常," + response.message));
return;
}
sink.next(response.data.get("token").toString());
})
.block();
}
/**
* 创建任务
*
* @param type 类型
* @param content 内容
* @param files 文件列表
* @return 任务 ID
* @see <a href="https://docmee.cn/open-platform/api#%E5%88%9B%E5%BB%BA%E4%BB%BB%E5%8A%A1">创建任务</a>
*/
public ApiResponse createTask(Integer type, String content, List<MultipartFile> files) {
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("type", type);
if (content != null) {
formData.add("content", content);
}
if (files != null) {
for (MultipartFile file : files) {
formData.add("file", file.getResource());
}
}
return this.webClient.post()
.uri("/api/ppt/v2/createTask")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(formData))
.bodyToMono(ApiResponse.class)
.block();
}
/**
* 获取生成选项
*
* @param lang 语种
* @return 生成选项
*/
public Map<String, Object> getOptions(String lang) {
String uri = "/api/ppt/v2/options";
if (lang != null) {
uri += "?lang=" + lang;
}
return this.webClient.get()
.uri(uri)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(lang))
.bodyToMono(new ParameterizedTypeReference<ApiResponse>() {
})
.<Map<String, Object>>handle((response, sink) -> {
if (response.code != 0) {
sink.error(new IllegalStateException("获取生成选项异常," + response.message));
return;
}
sink.next(response.data);
})
.block();
}
/**
* 分页查询 PPT 模板
*
* @param token 令牌
* @param request 请求体
* @return 模板列表
*/
public PagePptTemplateInfo getTemplatePage(TemplateQueryRequest request) {
return this.webClient.post()
.uri("/api/ppt/templates")
.bodyValue(request)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(new ParameterizedTypeReference<PagePptTemplateInfo>() {
})
.block();
}
/**
* 生成大纲内容
*
* @return 大纲内容流
*/
public Flux<Map<String, Object>> createOutline(CreateOutlineRequest request) {
return this.webClient.post()
.uri("/api/ppt/v2/generateContent")
.body(Mono.just(request), CreateOutlineRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToFlux(new ParameterizedTypeReference<>() {
});
}
/**
* 修改大纲内容
*
* @param request 请求体
* @return 大纲内容流
*/
public Flux<Map<String, Object>> updateOutline(UpdateOutlineRequest request) {
return this.webClient.post()
.uri("/api/ppt/v2/updateContent")
.body(Mono.just(request), UpdateOutlineRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToFlux(new ParameterizedTypeReference<>() {
});
}
/**
* 生成 PPT
*
* @return PPT信息
*/
public PptInfo create(PptCreateRequest request) {
return this.webClient.post()
.uri("/api/ppt/v2/generatePptx")
.body(Mono.just(request), PptCreateRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(ApiResponse.class)
.<PptInfo>handle((response, sink) -> {
if (response.code != 0) {
sink.error(new IllegalStateException("生成 PPT 异常," + response.message));
return;
}
sink.next(Objects.requireNonNull(JsonUtils.parseObject(JsonUtils.toJsonString(response.data.get("pptInfo")), PptInfo.class)));
})
.block();
}
/**
* 创建 Token 请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateTokenRequest(
String apiKey,
String uid,
Integer limit
) {
public CreateTokenRequest(String apiKey) {
this(apiKey, null, null);
}
}
/**
* API 通用响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ApiResponse(
Integer code,
String message,
Map<String, Object> data
) {
}
/**
* 创建任务
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateTaskRequest(
Integer type,
String content,
List<MultipartFile> files
) {
}
/**
* 生成大纲内容请求
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateOutlineRequest(
String id,
String length,
String scene,
String audience,
String lang,
String prompt
) {
}
/**
* 修改大纲内容请求
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record UpdateOutlineRequest(
String id,
String markdown,
String question
) {
}
/**
* 生成 PPT 请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PptCreateRequest(
String id,
String templateId,
String markdown
) {
}
/**
* PPT 信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PptInfo(
String id,
String name,
String subject,
String coverUrl,
String fileUrl,
String templateId,
String pptxProperty,
String userId,
String userName,
int companyId,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime updateTime,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createTime,
String createUser,
String updateUser
) {
}
/**
* 模板查询请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplateQueryRequest(
int page,
int size,
Filter filters
) {
/**
* 模板查询过滤条件
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record Filter(
int type,
String category,
String style,
String themeColor
) {
}
}
/**
* PPT模板分页信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PagePptTemplateInfo(
List<PptTemplateInfo> data,
String total
) {
}
/**
* PPT模板信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PptTemplateInfo(
String id,
int type,
Integer subType,
String layout,
String category,
String style,
String themeColor,
String lang,
boolean animation,
String subject,
String coverUrl,
String fileUrl,
List<String> pageCoverUrls,
String pptxProperty,
int sort,
int num,
Integer imgNum,
int isDeleted,
String userId,
int companyId,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime updateTime,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createTime,
String createUser,
String updateUser
) {
}
}

View File

@@ -0,0 +1,522 @@
package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 讯飞智能 PPT 生成 API
*
* @author xiaoxin
* @see <a href="https://www.xfyun.cn/doc/spark/PPTv2.html">智能 PPT 生成 API</a>
*/
@Slf4j
public class XunFeiPptApi {
public static final String BASE_URL = "https://zwapi.xfyun.cn/api/ppt/v2";
private static final String HEADER_APP_ID = "appId";
private static final String HEADER_TIMESTAMP = "timestamp";
private static final String HEADER_SIGNATURE = "signature";
private final WebClient webClient;
private final String appId;
private final String apiSecret;
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
log.error("[XunFeiPptApi] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody);
sink.error(new IllegalStateException("[XunFeiPptApi] 调用失败!"));
});
public XunFeiPptApi(String appId, String apiSecret) {
this.appId = appId;
this.apiSecret = apiSecret;
this.webClient = WebClient.builder()
.baseUrl(BASE_URL)
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(HEADER_APP_ID, appId);
})
.build();
}
/**
* 获取签名
*
* @return 签名信息
*/
private SignatureInfo getSignature() {
long timestamp = System.currentTimeMillis() / 1000;
String ts = String.valueOf(timestamp);
String signature = generateSignature(appId, apiSecret, timestamp);
return new SignatureInfo(ts, signature);
}
/**
* 生成签名
*
* @param appId 应用ID
* @param apiSecret 应用密钥
* @param timestamp 时间戳(秒)
* @return 签名
*/
private String generateSignature(String appId, String apiSecret, long timestamp) {
String auth = SecureUtil.md5(appId + timestamp);
return SecureUtil.hmac(HmacAlgorithm.HmacSHA1, apiSecret).digestBase64(auth, false);
}
/**
* 获取 PPT 模板列表
*
* @param style 风格,如"商务"
* @param pageSize 每页数量
* @return 模板列表
*/
public TemplatePageResponse getTemplatePage(String style, Integer pageSize) {
SignatureInfo signInfo = getSignature();
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("style", style);
requestBody.put("pageSize", ObjUtil.defaultIfNull(pageSize, 20));
return this.webClient.post()
.uri("/template/list")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(requestBody))
.bodyToMono(TemplatePageResponse.class)
.block();
}
/**
* 创建大纲(通过文本)
*
* @param query 查询文本
* @return 大纲创建响应
*/
public CreateResponse createOutline(String query) {
SignatureInfo signInfo = getSignature();
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("query", query);
return this.webClient.post()
.uri("/createOutline")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(formData))
.bodyToMono(CreateResponse.class)
.block();
}
/**
* 直接创建 PPT简化版 - 通过文本)
*
* @param query 查询文本
* @return 创建响应
*/
public CreateResponse create(String query) {
CreatePptRequest request = CreatePptRequest.builder()
.query(query)
.build();
return create(request);
}
/**
* 直接创建 PPT简化版 - 通过文件)
*
* @param file 文件
* @param fileName 文件名
* @return 创建响应
*/
public CreateResponse create(MultipartFile file, String fileName) {
CreatePptRequest request = CreatePptRequest.builder()
.file(file).fileName(fileName).build();
return create(request);
}
/**
* 直接创建 PPT完整版
*
* @param request 请求参数
* @return 创建响应
*/
public CreateResponse create(CreatePptRequest request) {
SignatureInfo signInfo = getSignature();
MultiValueMap<String, Object> formData = buildCreatePptFormData(request);
return this.webClient.post()
.uri("/create")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(formData))
.bodyToMono(CreateResponse.class)
.block();
}
/**
* 通过大纲创建 PPT简化版
*
* @param outline 大纲内容
* @param query 查询文本
* @return 创建响应
*/
public CreateResponse createPptByOutline(OutlineData outline, String query) {
CreatePptByOutlineRequest request = CreatePptByOutlineRequest.builder()
.outline(outline)
.query(query)
.build();
return createPptByOutline(request);
}
/**
* 通过大纲创建 PPT完整版
*
* @param request 请求参数
* @return 创建响应
*/
public CreateResponse createPptByOutline(CreatePptByOutlineRequest request) {
SignatureInfo signInfo = getSignature();
return this.webClient.post()
.uri("/createPptByOutline")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(CreateResponse.class)
.block();
}
/**
* 检查 PPT 生成进度
*
* @param sid 任务 ID
* @return 进度响应
*/
public ProgressResponse checkProgress(String sid) {
SignatureInfo signInfo = getSignature();
return this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/progress")
.queryParam("sid", sid)
.build())
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(sid))
.bodyToMono(ProgressResponse.class)
.block();
}
/**
* 签名信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
private record SignatureInfo(
String timestamp,
String signature
) {
}
/**
* 模板列表响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplatePageResponse(
boolean flag,
int code,
String desc,
Integer count,
TemplatePageData data
) {
}
/**
* 模板列表数据
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplatePageData(
String total,
List<TemplateInfo> records,
Integer pageNum
) {
}
/**
* 模板信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplateInfo(
String templateIndexId,
Integer pageCount,
String type,
String color,
String industry,
String style,
String detailImage
) {
}
/**
* 创建响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateResponse(
boolean flag,
int code,
String desc,
Integer count,
CreateResponseData data
) {
}
/**
* 创建响应数据
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateResponseData(
String sid,
String coverImgSrc,
String title,
String subTitle,
OutlineData outline
) {
}
/**
* 大纲数据结构
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record OutlineData(
String title,
String subTitle,
List<Chapter> chapters
) {
/**
* 章节结构
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record Chapter(
String chapterTitle,
List<ChapterContent> chapterContents
) {
/**
* 章节内容
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ChapterContent(
String chapterTitle
) {
}
}
/**
* 将大纲对象转换为JSON字符串
*
* @return 大纲JSON字符串
*/
public String toJsonString() {
return JsonUtils.toJsonString(this);
}
}
/**
* 进度响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ProgressResponse(
int code,
String desc,
ProgressResponseData data
) {
}
/**
* 进度响应数据
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ProgressResponseData(
int process,
String pptId,
String pptUrl,
String pptStatus,
String aiImageStatus,
String cardNoteStatus,
String errMsg,
Integer totalPages,
Integer donePages
) {
/**
* 是否全部完成
*
* @return 是否全部完成
*/
public boolean isAllDone() {
return "done".equals(pptStatus)
&& ("done".equals(aiImageStatus) || aiImageStatus == null)
&& ("done".equals(cardNoteStatus) || cardNoteStatus == null);
}
/**
* 是否失败
*
* @return 是否失败
*/
public boolean isFailed() {
return "build_failed".equals(pptStatus);
}
/**
* 获取进度百分比
*
* @return 进度百分比
*/
public int getProgressPercent() {
if (totalPages == null || totalPages == 0 || donePages == null) {
return process; // 兼容旧版返回
}
return (int) (donePages * 100.0 / totalPages);
}
}
/**
* 通过大纲创建 PPT 请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@Builder
public record CreatePptByOutlineRequest(
String query, // 用户生成PPT要求最多8000字
String outlineSid, // 已生成大纲后响应返回的请求大纲唯一id
OutlineData outline, // 大纲内容
String templateId, // 模板ID
String businessId, // 业务ID非必传
String author, // PPT作者名
Boolean isCardNote, // 是否生成PPT演讲备注
Boolean search, // 是否联网搜索
String language, // 语种
String fileUrl, // 文件地址
String fileName, // 文件名(带文件名后缀)
Boolean isFigure, // 是否自动配图
String aiImage // ai配图类型normal、advanced
) {
}
/**
* 构建创建 PPT 的表单数据
*
* @param request 请求参数
* @return 表单数据
*/
private MultiValueMap<String, Object> buildCreatePptFormData(CreatePptRequest request) {
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
if (request.file() != null) {
try {
formData.add("file", new ByteArrayResource(request.file().getBytes()) {
@Override
public String getFilename() {
return request.file().getOriginalFilename();
}
});
} catch (IOException e) {
log.error("[XunFeiPptApi] 文件处理失败", e);
throw new IllegalStateException("[XunFeiPptApi] 文件处理失败", e);
}
}
Map<String, Object> param = new HashMap<>();
addIfPresent(param, "query", request.query());
addIfPresent(param, "fileUrl", request.fileUrl());
addIfPresent(param, "fileName", request.fileName());
addIfPresent(param, "templateId", request.templateId());
addIfPresent(param, "businessId", request.businessId());
addIfPresent(param, "author", request.author());
addIfPresent(param, "isCardNote", request.isCardNote());
addIfPresent(param, "search", request.search());
addIfPresent(param, "language", request.language());
addIfPresent(param, "isFigure", request.isFigure());
addIfPresent(param, "aiImage", request.aiImage());
param.forEach(formData::add);
return formData;
}
public static <K, V> void addIfPresent(Map<K, V> map, K key, V value) {
if (ObjUtil.isNull(key) || ObjUtil.isNull(map)) {
return;
}
boolean isPresent = false;
if (ObjUtil.isNotNull(value)) {
if (value instanceof String) {
// 字符串:需要有实际内容
isPresent = StringUtils.hasText((String) value);
} else {
// 其他类型:非 null 即视为存在
isPresent = true;
}
}
if (isPresent) {
map.put(key, value);
}
}
/**
* 直接生成PPT请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@Builder
public record CreatePptRequest(
String query, // 用户生成PPT要求最多8000字
MultipartFile file, // 上传文件
String fileUrl, // 文件地址
String fileName, // 文件名(带文件名后缀)
String templateId, // 模板ID
String businessId, // 业务ID非必传
String author, // PPT作者名
Boolean isCardNote, // 是否生成PPT演讲备注
Boolean search, // 是否联网搜索
String language, // 语种
Boolean isFigure, // 是否自动配图
String aiImage // ai配图类型normal、advanced
) {
}
}

View File

@@ -53,6 +53,7 @@ public class AiUtils {
case HUN_YUAN: // 复用 OpenAI 客户端
case XING_HUO: // 复用 OpenAI 客户端
case SILICON_FLOW: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).build();
case AZURE_OPENAI:

View File

@@ -0,0 +1,68 @@
package cn.iocoder.yudao.framework.ai.chat;
import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* {@link BaiChuanChatModel} 集成测试
*
* @author 芋道源码
*/
public class BaiChuanChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey("sk-61b6766a94c70786ed02673f5e16af3c") // apiKey
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model("Baichuan4-Turbo") // 模型https://platform.baichuan-ai.com/docs/api
.temperature(0.7)
.build())
.build();
private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
@Test
@Disabled
public void testCall() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
ChatResponse response = chatModel.call(new Prompt(messages));
// 打印结果
System.out.println(response);
}
@Test
@Disabled
public void testStream() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(System.out::println).then().block();
}
}

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.ai.chat;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -25,11 +26,11 @@ public class SiliconFlowChatModelTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(SiliconFlowChatModel.BASE_URL)
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(SiliconFlowChatModel.MODEL_DEFAULT) // 模型
.model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型
// .model("deepseek-ai/DeepSeek-R1") // 模型deepseek-ai/DeepSeek-R1可用赠费
// .model("Pro/deepseek-ai/DeepSeek-R1") // 模型Pro/deepseek-ai/DeepSeek-R1需要付费
.temperature(0.7)

View File

@@ -0,0 +1,35 @@
package cn.iocoder.yudao.framework.ai.image;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
/**
* {@link SiliconFlowImageModel} 集成测试
*/
public class SiliconFlowImageModelTests {
private final SiliconFlowImageModel imageModel = new SiliconFlowImageModel(
new SiliconFlowImageApi("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // 密钥
);
@Test
@Disabled
public void testCall() {
// 准备参数
SiliconFlowImageOptions imageOptions = SiliconFlowImageOptions.builder()
.model("Kwai-Kolors/Kolors")
.build();
ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions);
// 方法调用
ImageResponse response = imageModel.call(prompt);
// 打印结果
System.out.println(response);
}
}

View File

@@ -0,0 +1,122 @@
package cn.iocoder.yudao.framework.ai.mcp;
import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
public class DouBaoMcpTests {
private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
.apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model("doubao-1-5-lite-32k-250115") // 模型doubao
.temperature(0.7)
.build())
.build();
private final DouBaoChatModel chatModel = new DouBaoChatModel(openAiChatModel);
private final MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
.toolObjects(new UserService())
.build();
private final ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(provider)
.build();
@Test
public void testMcpGetUserInfo() {
// 打印结果
System.out.println(chatClient.prompt()
.user("目前有哪些工具可以使用")
.call()
.content());
System.out.println("====================================");
// 打印结果
System.out.println(chatClient.prompt()
.user("小新的年龄是多少")
.call()
.content());
System.out.println("====================================");
// 打印结果
System.out.println(chatClient.prompt()
.user("获取小新的基本信息")
.call()
.content());
System.out.println("====================================");
// 打印结果
System.out.println(chatClient.prompt()
.user("小新是什么职业的")
.call()
.content());
System.out.println("====================================");
// 打印结果
System.out.println(chatClient.prompt()
.user("小新的教育背景")
.call()
.content());
System.out.println("====================================");
// 打印结果
System.out.println(chatClient.prompt()
.user("小新的兴趣爱好是什么")
.call()
.content());
System.out.println("====================================");
}
static class UserService {
@Tool(name = "getUserAge", description = "获取用户年龄")
public String getUserAge(String userName) {
return "" + userName + "》的年龄为18";
}
@Tool(name = "getUserSex", description = "获取用户性别")
public String getUserSex(String userName) {
return "" + userName + "》的性别为:男";
}
@Tool(name = "getUserBasicInfo", description = "获取用户基本信息,包括姓名、年龄、性别等")
public String getUserBasicInfo(String userName) {
return "" + userName + "》的基本信息:\n姓名" + userName + "\n年龄18\n性别\n身高175cm\n体重65kg";
}
@Tool(name = "getUserContact", description = "获取用户联系方式,包括电话、邮箱等")
public String getUserContact(String userName) {
return "" + userName + "》的联系方式:\n电话138****1234\n邮箱" + userName.toLowerCase() + "@example.com\nQQ123456789";
}
@Tool(name = "getUserAddress", description = "获取用户地址信息")
public String getUserAddress(String userName) {
return "" + userName + "》的地址信息北京市朝阳区科技园区88号";
}
@Tool(name = "getUserJob", description = "获取用户职业信息")
public String getUserJob(String userName) {
return "" + userName + "》的职业信息软件工程师就职于ABC科技有限公司工作年限5年";
}
@Tool(name = "getUserHobbies", description = "获取用户兴趣爱好")
public String getUserHobbies(String userName) {
return "" + userName + "》的兴趣爱好:编程、阅读、旅游、摄影、打篮球";
}
@Tool(name = "getUserEducation", description = "获取用户教育背景")
public String getUserEducation(String userName) {
return "" + userName + "》的教育背景:\n本科计算机科学与技术专业北京大学\n硕士软件工程专业清华大学";
}
}
}

View File

@@ -0,0 +1,314 @@
package cn.iocoder.yudao.framework.ai.ppt.wdd;
import cn.iocoder.yudao.framework.ai.core.model.wenduoduo.api.WenDuoDuoPptApi;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import java.util.Map;
import java.util.Objects;
/**
* {@link WenDuoDuoPptApi} 集成测试
*
* @author xiaoxin
*/
public class WenDuoDuoPptApiTests {
private final String token = ""; // API Token
private final WenDuoDuoPptApi wenDuoDuoPptApi = new WenDuoDuoPptApi(token);
@Test
@Disabled
public void testCreateApiToken() {
// 准备参数
String apiKey = "";
WenDuoDuoPptApi.CreateTokenRequest request = new WenDuoDuoPptApi.CreateTokenRequest(apiKey);
// 调用方法
String token = wenDuoDuoPptApi.createApiToken(request);
// 打印结果
System.out.println(token);
}
/**
* 创建任务
*/
@Test
@Disabled
public void testCreateTask() {
WenDuoDuoPptApi.ApiResponse apiResponse = wenDuoDuoPptApi.createTask(1, "dify 介绍", null);
System.out.println(apiResponse);
}
@Test // 创建大纲
@Disabled
public void testGenerateOutlineRequest() {
WenDuoDuoPptApi.CreateOutlineRequest request = new WenDuoDuoPptApi.CreateOutlineRequest(
"1901539019628613632", "medium", null, null, null, null);
// 调用
Flux<Map<String, Object>> flux = wenDuoDuoPptApi.createOutline(request);
StringBuffer contentBuffer = new StringBuffer();
flux.doOnNext(chunk -> {
contentBuffer.append(chunk.get("text"));
if (Objects.equals(Integer.parseInt(String.valueOf(chunk.get("status"))), 4)) {
// status 为 4最终 markdown 结构树
System.out.println(JsonUtils.toJsonString(chunk.get("result")));
System.out.println(" ########################################################################");
}
}).then().block();
// 打印结果
System.out.println(contentBuffer);
}
/**
* 修改大纲
*/
@Test
@Disabled
public void testUpdateOutlineRequest() {
WenDuoDuoPptApi.UpdateOutlineRequest request = new WenDuoDuoPptApi.UpdateOutlineRequest(
"1901539019628613632", TEST_OUT_LINE_CONTENT, "精简一点,三个章节即可");
// 调用
Flux<Map<String, Object>> flux = wenDuoDuoPptApi.updateOutline(request);
StringBuffer contentBuffer = new StringBuffer();
flux.doOnNext(chunk -> {
contentBuffer.append(chunk.get("text"));
if (Objects.equals(Integer.parseInt(String.valueOf(chunk.get("status"))), 4)) {
// status 为 4最终 markdown 结构树
System.out.println(JsonUtils.toJsonString(chunk.get("result")));
System.out.println(" ########################################################################");
}
}).then().block();
// 打印结果
System.out.println(contentBuffer);
}
/**
* 获取 PPT 模版分页
*/
@Test
@Disabled
public void testGetPptTemplatePage() {
// 准备参数
WenDuoDuoPptApi.TemplateQueryRequest.Filter filter = new WenDuoDuoPptApi.TemplateQueryRequest.Filter(
1, null, null, null);
WenDuoDuoPptApi.TemplateQueryRequest request = new WenDuoDuoPptApi.TemplateQueryRequest(1, 10, filter);
// 调用
WenDuoDuoPptApi.PagePptTemplateInfo pptTemplatePage = wenDuoDuoPptApi.getTemplatePage(request);
// 打印结果
System.out.println(pptTemplatePage);
}
/**
* 生成 PPT
*/
@Test
@Disabled
public void testGeneratePptx() {
// 准备参数
WenDuoDuoPptApi.PptCreateRequest request = new WenDuoDuoPptApi.PptCreateRequest("1901539019628613632", "1805081814809960448", TEST_OUT_LINE_CONTENT);
// 调用
WenDuoDuoPptApi.PptInfo pptInfo = wenDuoDuoPptApi.create(request);
// 打印结果
System.out.println(pptInfo);
}
private final String TEST_OUT_LINE_CONTENT = """
# Dify新一代AI应用开发平台
## 1 什么是Dify
### 1.1 Dify定义AI应用开发平台
#### 1.1.1 低代码开发
Dify是一个低代码AI应用开发平台旨在简化AI应用的构建过程让开发者无需编写大量代码即可快速创建各种智能应用。
#### 1.1.2 核心功能
Dify的核心功能包括数据集成、模型选择、流程编排和应用部署提供一站式解决方案加速AI应用的落地和迭代。
#### 1.1.3 开源与商业
Dify提供开源版本和商业版本满足不同用户的需求开源版本适合个人开发者和小型团队商业版本则提供更强大的功能和技术支持。
### 1.2 Dify解决的问题AI开发痛点
#### 1.2.1 开发周期长
传统AI应用开发周期长需要大量的人力和时间投入Dify通过可视化界面和预置组件大幅缩短开发周期。
#### 1.2.2 技术门槛高
AI技术门槛高需要专业的知识和技能Dify降低技术门槛让更多开发者能够参与到AI应用的开发中来。
#### 1.2.3 部署和维护复杂
AI应用的部署和维护复杂需要专业的运维团队Dify提供自动化的部署和维护工具简化流程降低成本。
### 1.3 Dify发展历程
#### 1.3.1 早期探索
Dify的早期版本主要关注于自然语言处理领域的应用通过集成各种NLP模型提供文本分类、情感分析等功能。
#### 1.3.2 功能扩展
随着用户需求的不断增长Dify的功能逐渐扩展到图像识别、语音识别等领域支持更多类型的AI应用。
#### 1.3.3 生态建设
Dify积极建设开发者生态提供丰富的文档、教程和案例帮助开发者更好地使用Dify平台共同推动AI技术的发展。
## 2 Dify的核心功能
### 2.1 数据集成:连接各种数据源
#### 2.1.1 支持多种数据源
Dify支持连接各种数据源包括关系型数据库、NoSQL数据库、文件系统、云存储等满足不同场景的数据需求。
#### 2.1.2 数据转换和清洗
Dify提供数据转换和清洗功能可以将不同格式的数据转换为统一的格式并去除无效数据提高数据质量。
#### 2.1.3 数据安全
Dify注重数据安全采用各种安全措施保护用户的数据包括数据加密、访问控制、权限管理等。
### 2.2 模型选择丰富的AI模型库
#### 2.2.1 预置模型
Dify预置了丰富的AI模型包括自然语言处理、图像识别、语音识别等领域的模型开发者可以直接使用这些模型无需自行训练极大的简化了开发流程。
#### 2.2.2 自定义模型
Dify支持开发者上传自定义模型满足个性化的需求。开发者可以将自己训练的模型部署到Dify平台上与其他开发者共享。
#### 2.2.3 模型评估
Dify提供模型评估功能可以对不同模型进行评估选择最优的模型提高应用性能。
### 2.3 流程编排:可视化流程设计器
#### 2.3.1 可视化界面
Dify提供可视化的流程设计器开发者可以通过拖拽组件的方式设计AI应用的流程无需编写代码简单高效。
#### 2.3.2 灵活的流程控制
Dify支持灵活的流程控制可以根据不同的条件执行不同的分支实现复杂的业务逻辑。
#### 2.3.3 实时调试
Dify提供实时调试功能可以在设计流程的过程中实时查看流程的执行结果及时发现和解决问题。
### 2.4 应用部署:一键部署和管理
#### 2.4.1 快速部署
Dify提供一键部署功能可以将AI应用快速部署到各种环境包括本地环境、云环境、容器环境等。
#### 2.4.2 自动伸缩
Dify支持自动伸缩可以根据应用的负载自动调整资源保证应用的稳定性和性能。
#### 2.4.3 监控和告警
Dify提供监控和告警功能可以实时监控应用的状态并在出现问题时及时告警方便运维人员进行处理。
## 3 Dify的特点和优势
### 3.1 低代码:降低开发门槛
#### 3.1.1 可视化开发
Dify采用可视化开发模式开发者无需编写大量代码只需通过拖拽组件即可完成AI应用的开发降低了开发门槛。
#### 3.1.2 预置组件
Dify预置了丰富的组件包括数据源组件、模型组件、流程控制组件等开发者可以直接使用这些组件提高开发效率。
#### 3.1.3 减少代码量
Dify可以显著减少代码量降低开发难度让更多开发者能够参与到AI应用的开发中来。
### 3.2 灵活:满足不同场景需求
#### 3.2.1 支持多种数据源
Dify支持多种数据源可以连接各种数据源满足不同场景的数据需求。
#### 3.2.2 支持自定义模型
Dify支持自定义模型开发者可以将自己训练的模型部署到Dify平台上满足个性化的需求。
#### 3.2.3 灵活的流程控制
Dify支持灵活的流程控制可以根据不同的条件执行不同的分支实现复杂的业务逻辑。
### 3.3 高效:加速应用落地
#### 3.3.1 快速开发
Dify通过可视化界面和预置组件大幅缩短开发周期加速AI应用的落地。
#### 3.3.2 快速部署
Dify提供一键部署功能可以将AI应用快速部署到各种环境提高部署效率。
#### 3.3.3 自动化运维
Dify提供自动化的运维工具简化运维流程降低运维成本。
### 3.4 开放:构建繁荣生态
#### 3.4.1 开源社区
Dify拥有活跃的开源社区开发者可以在社区中交流经验、分享资源、共同推动Dify的发展。
#### 3.4.2 丰富的文档
Dify提供丰富的文档、教程和案例帮助开发者更好地使用Dify平台。
#### 3.4.3 API支持
Dify提供API支持开发者可以通过API将Dify集成到自己的系统中扩展Dify的功能。
## 4 Dify的使用场景
### 4.1 智能客服:提升客户服务质量
#### 4.1.1 自动回复
Dify可以用于构建智能客服系统实现自动回复客户的常见问题提高客户服务效率。
#### 4.1.2 情感分析
Dify可以对客户的语音或文本进行情感分析判断客户的情绪并根据情绪提供个性化的服务。
#### 4.1.3 知识库问答
Dify可以构建知识库问答系统让客户通过提问的方式获取所需的信息提高客户满意度。
### 4.2 金融风控:提高风险识别能力
#### 4.2.1 欺诈检测
Dify可以用于构建金融风控系统实现欺诈检测识别可疑交易降低风险。
#### 4.2.2 信用评估
Dify可以对用户的信用进行评估并根据评估结果提供不同的金融服务。
#### 4.2.3 反洗钱
Dify可以用于反洗钱识别可疑资金流动防止犯罪行为。
### 4.3 智慧医疗:提升医疗服务水平
#### 4.3.1 疾病诊断
Dify可以用于辅助疾病诊断提高诊断准确率缩短诊断时间。
#### 4.3.2 药物研发
Dify可以用于药物研发加速新药的发现和开发。
#### 4.3.3 智能健康管理
Dify可以构建智能健康管理系统为用户提供个性化的健康建议和服务。
### 4.4 智慧城市:提升城市管理效率
#### 4.4.1 交通优化
Dify可以用于交通优化提高交通效率缓解交通拥堵。
#### 4.4.2 环境监测
Dify可以用于环境监测实时监测空气质量、水质等环境指标及时发现和解决环境问题。
#### 4.4.3 智能安防
Dify可以用于智能安防提高城市安全水平预防犯罪行为。
## 5 Dify的成功案例
### 5.1 Case 1某电商平台的智能客服
#### 5.1.1 项目背景
该电商平台客户服务压力大,人工客服成本高,需要一种智能化的解决方案。
#### 5.1.2 解决方案
使用Dify构建智能客服系统实现自动回复客户的常见问题并根据客户的情绪提供个性化的服务。
#### 5.1.3 效果
客户服务效率提高50%客户满意度提高20%人工客服成本降低30%。
### 5.2 Case 2某银行的金融风控系统
#### 5.2.1 项目背景
该银行面临日益增长的金融风险,需要一种更有效的风险识别和控制手段。
#### 5.2.2 解决方案
使用Dify构建金融风控系统实现欺诈检测、信用评估和反洗钱等功能提高风险识别能力。
#### 5.2.3 效果
欺诈交易识别率提高40%信用评估准确率提高30%洗钱风险降低25%。
### 5.3 Case 3某医院的辅助疾病诊断系统
#### 5.3.1 项目背景
该医院医生工作压力大,疾病诊断准确率有待提高,需要一种辅助诊断工具。
#### 5.3.2 解决方案
使用Dify构建辅助疾病诊断系统根据患者的病历和症状提供诊断建议提高诊断准确率。
#### 5.3.3 效果
疾病诊断准确率提高20%诊断时间缩短15%医生工作效率提高10%。
## 6 Dify的未来展望
### 6.1 技术升级
#### 6.1.1 模型优化
Dify将不断优化预置模型提高模型性能并支持更多类型的AI模型。
#### 6.1.2 流程引擎升级
Dify将升级流程引擎提高流程的灵活性和可扩展性支持更复杂的业务逻辑。
#### 6.1.3 平台性能优化
Dify将不断优化平台性能提高平台的稳定性和可靠性满足大规模应用的需求。
### 6.2 生态建设
#### 6.2.1 社区建设
Dify将继续加强开源社区建设吸引更多开发者参与共同推动Dify的发展。
#### 6.2.2 合作伙伴拓展
Dify将拓展合作伙伴与更多的企业和机构合作共同推动AI技术的应用。
#### 6.2.3 应用商店
Dify将构建应用商店让开发者可以分享自己的应用用户可以购买和使用这些应用构建繁荣的生态系统。
### 6.3 应用领域拓展
#### 6.3.1 智能制造
Dify将拓展到智能制造领域为企业提供智能化的生产管理和质量控制解决方案。
#### 6.3.2 智慧农业
Dify将拓展到智慧农业领域为农民提供智能化的种植和养殖管理解决方案。
#### 6.3.3 更多领域
Dify将拓展到更多领域为各行各业提供智能化的解决方案推动社会发展。
## 7 总结
### 7.1 Dify的价值
#### 7.1.1 降低AI开发门槛
Dify通过低代码的方式让更多开发者能够参与到AI应用的开发中来。
#### 7.1.2 加速AI应用落地
Dify提供一站式解决方案加速AI应用的落地和迭代。
#### 7.1.3 构建繁荣的AI生态
Dify通过开源社区和应用商店构建繁荣的AI生态系统。
### 7.2 共同发展
#### 7.2.1 欢迎加入Dify社区
欢迎更多开发者加入Dify社区共同推动Dify的发展。
#### 7.2.2 合作共赢
期待与更多的企业和机构合作共同推动AI技术的应用。
#### 7.2.3 共创未来
让我们一起用AI技术改变世界共创美好未来。
""";
}

View File

@@ -0,0 +1,319 @@
package cn.iocoder.yudao.framework.ai.ppt.xunfei;
import cn.hutool.core.io.FileUtil;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XunFeiPptApi;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
/**
* {@link XunFeiPptApi} 集成测试
*
* @author xiaoxin
*/
public class XunFeiPptApiTests {
// 讯飞 API 配置信息,实际使用时请替换为您的应用信息
private static final String APP_ID = "6c8ac023";
private static final String API_SECRET = "Y2RjM2Q1MWJjZTdkYmFiODc0OGE5NmRk";
private final XunFeiPptApi xunfeiPptApi = new XunFeiPptApi(APP_ID, API_SECRET);
/**
* 获取 PPT 模板列表
*/
@Test
@Disabled
public void testGetTemplatePage() {
// 调用方法
XunFeiPptApi.TemplatePageResponse response = xunfeiPptApi.getTemplatePage("商务", 10);
// 打印结果
System.out.println("模板列表响应:" + JsonUtils.toJsonString(response));
if (response != null && response.data() != null && response.data().records() != null) {
System.out.println("模板总数:" + response.data().total());
System.out.println("当前页码:" + response.data().pageNum());
System.out.println("模板数量:" + response.data().records().size());
// 打印第一个模板的信息(如果存在)
if (!response.data().records().isEmpty()) {
XunFeiPptApi.TemplateInfo firstTemplate = response.data().records().get(0);
System.out.println("模板ID" + firstTemplate.templateIndexId());
System.out.println("模板风格:" + firstTemplate.style());
System.out.println("模板颜色:" + firstTemplate.color());
System.out.println("模板行业:" + firstTemplate.industry());
}
}
}
/**
* 创建大纲(通过文本)
*/
@Test
@Disabled
public void testCreateOutline() {
XunFeiPptApi.CreateResponse response = getCreateResponse();
// 打印结果
System.out.println("创建大纲响应:" + JsonUtils.toJsonString(response));
// 保存 sid 和 outline 用于后续测试
if (response != null && response.data() != null) {
System.out.println("sid: " + response.data().sid());
if (response.data().outline() != null) {
// 使用 OutlineData 的 toJsonString 方法
System.out.println("outline: " + response.data().outline().toJsonString());
// 将 outline 对象转换为 JSON 字符串,用于后续 createPptByOutline 测试
String outlineJson = response.data().outline().toJsonString();
System.out.println("可用于 createPptByOutline 的 outline 字符串: " + outlineJson);
}
}
}
/**
* 创建大纲(通过文本)
*
* @return 创建大纲响应
*/
private XunFeiPptApi.CreateResponse getCreateResponse() {
String param = "智能体平台 Dify 介绍";
return xunfeiPptApi.createOutline(param);
}
/**
* 通过大纲创建 PPT完整参数
*/
@Test
@Disabled
public void testCreatePptByOutlineWithFullParams() {
// 创建大纲对象
XunFeiPptApi.CreateResponse createResponse = getCreateResponse();
// 调用方法
XunFeiPptApi.CreateResponse response = xunfeiPptApi.createPptByOutline(createResponse.data().outline(), "精简一些不要超过6个章节");
// 打印结果
System.out.println("通过大纲创建 PPT 响应:" + JsonUtils.toJsonString(response));
// 保存sid用于后续进度查询
if (response != null && response.data() != null) {
System.out.println("sid: " + response.data().sid());
if (response.data().coverImgSrc() != null) {
System.out.println("封面图片: " + response.data().coverImgSrc());
}
}
}
/**
* 检查 PPT 生成进度
*/
@Test
@Disabled
public void testCheckProgress() {
// 准备参数 - 使用之前创建 PPT 时返回的 sid
String sid = "e96dac09f2ec4ee289f029a5fb874ecd"; // 替换为实际的sid
// 调用方法
XunFeiPptApi.ProgressResponse response = xunfeiPptApi.checkProgress(sid);
// 打印结果
System.out.println("检查进度响应:" + JsonUtils.toJsonString(response));
// 安全地访问响应数据
if (response != null && response.data() != null) {
XunFeiPptApi.ProgressResponseData data = response.data();
// 打印PPT生成状态
System.out.println("PPT 构建状态: " + data.pptStatus());
System.out.println("AI 配图状态: " + data.aiImageStatus());
System.out.println("演讲备注状态: " + data.cardNoteStatus());
// 打印进度信息
if (data.totalPages() != null && data.donePages() != null) {
System.out.println("总页数: " + data.totalPages());
System.out.println("已完成页数: " + data.donePages());
System.out.println("完成进度: " + data.getProgressPercent() + "%");
} else {
System.out.println("进度: " + data.process() + "%");
}
// 检查是否完成
if (data.isAllDone()) {
System.out.println("PPT 生成已完成!");
System.out.println("PPT 下载链接: " + data.pptUrl());
}
// 检查是否失败
else if (data.isFailed()) {
System.out.println("PPT 生成失败!");
System.out.println("错误信息: " + data.errMsg());
}
// 正在进行中
else {
System.out.println("PPT 生成中,请稍后再查询...");
}
}
}
/**
* 轮询检查 PPT 生成进度直到完成
*/
@Test
@Disabled
public void testPollCheckProgress() throws InterruptedException {
// 准备参数 - 使用之前创建 PP T时返回的 sid
String sid = "1690ef6ee0344e72b5c5434f403b8eaa"; // 替换为实际的sid
// 最大轮询次数
int maxPolls = 20;
// 轮询间隔(毫秒)- 讯飞 API 限流为 3 秒一次
long pollInterval = 3500;
for (int i = 0; i < maxPolls; i++) {
System.out.println("" + (i + 1) + "次查询进度...");
// 调用方法
XunFeiPptApi.ProgressResponse response = xunfeiPptApi.checkProgress(sid);
// 安全地访问响应数据
if (response != null && response.data() != null) {
XunFeiPptApi.ProgressResponseData data = response.data();
// 打印进度信息
System.out.println("PPT 构建状态: " + data.pptStatus());
if (data.totalPages() != null && data.donePages() != null) {
System.out.println("完成进度: " + data.donePages() + "/" + data.totalPages()
+ " (" + data.getProgressPercent() + "%)");
}
// 检查是否完成
if (data.isAllDone()) {
System.out.println("PPT 生成已完成!");
System.out.println("PPT 下载链接: " + data.pptUrl());
break;
}
// 检查是否失败
else if (data.isFailed()) {
System.out.println("PPT 生成失败!");
System.out.println("错误信息: " + data.errMsg());
break;
}
// 正在进行中,继续轮询
else {
System.out.println("PPT 生成中,等待" + (pollInterval / 1000) + "秒后继续查询...");
Thread.sleep(pollInterval);
}
} else {
System.out.println("查询失败,等待" + (pollInterval / 1000) + "秒后重试...");
Thread.sleep(pollInterval);
}
}
}
/**
* 直接创建 PPT通过文本
*/
@Test
@Disabled
public void testCreatePptByText() {
// 准备参数
String query = "合肥天气趋势分析包括近5年的气温变化、降水量变化、极端天气事件以及对城市生活的影响";
// 调用方法
XunFeiPptApi.CreateResponse response = xunfeiPptApi.create(query);
// 打印结果
System.out.println("直接创建 PPT 响应:" + JsonUtils.toJsonString(response));
// 保存 sid 用于后续进度查询
if (response != null && response.data() != null) {
System.out.println("sid: " + response.data().sid());
if (response.data().coverImgSrc() != null) {
System.out.println("封面图片: " + response.data().coverImgSrc());
}
System.out.println("标题: " + response.data().title());
System.out.println("副标题: " + response.data().subTitle());
}
}
/**
* 直接创建 PPT通过文件
*/
@Test
@Disabled
public void testCreatePptByFile() {
// 准备参数
File file = new File("src/test/resources/test.txt"); // 请确保此文件存在
MultipartFile multipartFile = convertFileToMultipartFile(file);
// 调用方法
XunFeiPptApi.CreateResponse response = xunfeiPptApi.create(multipartFile, file.getName());
// 打印结果
System.out.println("通过文件创建PPT响应" + JsonUtils.toJsonString(response));
// 保存 sid 用于后续进度查询
if (response != null && response.data() != null) {
System.out.println("sid: " + response.data().sid());
if (response.data().coverImgSrc() != null) {
System.out.println("封面图片: " + response.data().coverImgSrc());
}
System.out.println("标题: " + response.data().title());
System.out.println("副标题: " + response.data().subTitle());
}
}
/**
* 直接创建 PPT完整参数
*/
@Test
@Disabled
public void testCreatePptWithFullParams() {
// 准备参数
String query = "合肥天气趋势分析,包括近 5 年的气温变化、降水量变化、极端天气事件,以及对城市生活的影响";
// 创建请求对象
XunFeiPptApi.CreatePptRequest request = XunFeiPptApi.CreatePptRequest.builder()
.query(query)
.language("cn")
.isCardNote(true)
.search(true)
.isFigure(true)
.aiImage("advanced")
.author("测试用户")
.build();
// 调用方法
XunFeiPptApi.CreateResponse response = xunfeiPptApi.create(request);
// 打印结果
System.out.println("使用完整参数创建 PPT 响应:" + JsonUtils.toJsonString(response));
// 保存 sid 用于后续进度查询
if (response != null && response.data() != null) {
String sid = response.data().sid();
System.out.println("sid: " + sid);
if (response.data().coverImgSrc() != null) {
System.out.println("封面图片: " + response.data().coverImgSrc());
}
System.out.println("标题: " + response.data().title());
System.out.println("副标题: " + response.data().subTitle());
// 立即查询一次进度
System.out.println("立即查询进度...");
XunFeiPptApi.ProgressResponse progressResponse = xunfeiPptApi.checkProgress(sid);
if (progressResponse != null && progressResponse.data() != null) {
XunFeiPptApi.ProgressResponseData progressData = progressResponse.data();
System.out.println("PPT 构建状态: " + progressData.pptStatus());
if (progressData.totalPages() != null && progressData.donePages() != null) {
System.out.println("完成进度: " + progressData.donePages() + "/" + progressData.totalPages()
+ " (" + progressData.getProgressPercent() + "%)");
}
}
}
}
/**
* 将 File 转换为 MultipartFile
*/
private MultipartFile convertFileToMultipartFile(File file) {
return new MockMultipartFile("file", file.getName(), "text/plain", FileUtil.readBytes(file));
}
}

View File

@@ -42,6 +42,7 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败");
ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置");
ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消");
// ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

View File

@@ -18,6 +18,7 @@ public enum BpmReasonEnum {
REJECT_TASK("审批不通过任务,原因:{}"), // 场景:用户审批不通过任务。修改文案时,需要注意 isRejectReason 方法
CANCEL_PROCESS_INSTANCE_BY_START_USER("用户主动取消流程,原因:{}"), // 场景:用户主动取消流程
CANCEL_PROCESS_INSTANCE_BY_ADMIN("管理员【{}】取消流程,原因:{}"), // 场景:管理员取消流程
CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS("子流程自动取消,原因:主流程已取消"),
// ========== 流程任务的独有原因 ==========

View File

@@ -112,7 +112,7 @@ public class BpmSimpleModelNodeVO {
/**
* 条件节点设置
*/
private ConditionSetting conditionSetting; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
private ConditionSetting conditionSetting; // 仅用于条件节点 BpmSimpleModelNodeTypeEnum.CONDITION_NODE
@Schema(description = "路由分支组", example = "[]")
private List<RouterSetting> routerGroups;
@@ -241,7 +241,7 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "条件设置")
@Data
@Valid
// 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
// 仅用于条件节点 BpmSimpleModelNodeTypeEnum.CONDITION_NODE
public static class ConditionSetting {
@Schema(description = "条件类型", example = "1")

View File

@@ -148,7 +148,6 @@ public class BpmProcessInstanceController {
processDefinition, processDefinitionInfo, startUser, dept));
}
// TODO @lesan【子流程】子流程如果取消主流程应该是通过、还是不通过哈还是禁用掉子流程的取消
@DeleteMapping("/cancel-by-start-user")
@Operation(summary = "用户取消流程实例", description = "取消发起的流程")
@PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')")

View File

@@ -72,6 +72,9 @@ public class BpmApprovalDetailRespVO {
@Schema(description = "候选人用户列表")
private List<UserSimpleBaseVO> candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表
@Schema(description = "流程编号", example = "8761d8e0-0922-11f0-bd37-00ff1db677bf")
private String processInstanceId; // 当且仅当该节点是子流程节点时才会有值CallActivity 的 processInstanceId 字段)
}
@Schema(description = "活动节点的任务信息")

View File

@@ -10,6 +10,8 @@ import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDateTime;
import static cn.hutool.core.date.DatePattern.*;
/**
* BPM 流程 Id 编码的 Redis DAO
*
@@ -32,16 +34,16 @@ public class BpmProcessIdRedisDAO {
String infix = "";
switch (processIdRule.getInfix()) {
case "DAY":
infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDD");
infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN);
break;
case "HOUR":
infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHH");
infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HH");
break;
case "MINUTE":
infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHHmm");
infix = DateUtil.format(LocalDateTime.now(), PURE_DATE_PATTERN + "HHmm");
break;
case "SECOND":
infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHHmmss");
infix = DateUtil.format(LocalDateTime.now(), PURE_DATETIME_PATTERN);
break;
}

View File

@@ -187,7 +187,7 @@ public class SimpleModelUtils {
/**
* 构建有附加节点的连线
*
* @param nodeId 当前节点 ID
* @param nodeId 当前节点 ID
* @param attachNodeId 附属节点 ID
* @param targetNodeId 目标节点 ID
*/
@@ -662,6 +662,10 @@ public class SimpleModelUtils {
* 构造条件表达式
*/
public static String buildConditionExpression(BpmSimpleModelNodeVO.ConditionSetting conditionSetting) {
// 并行网关不需要设置条件
if (conditionSetting == null) {
return null;
}
return buildConditionExpression(conditionSetting.getConditionType(), conditionSetting.getConditionExpression(),
conditionSetting.getConditionGroups());
}
@@ -813,7 +817,6 @@ public class SimpleModelUtils {
callActivity.setCalledElementType("key");
// 1. 是否异步
if (node.getChildProcessSetting().getAsync()) {
// TODO @lesan: 这里目前测试没有跳过执行call activity 后面的节点
callActivity.setAsynchronous(true);
}
@@ -959,8 +962,8 @@ public class SimpleModelUtils {
if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) {
// 查找满足条件的 BpmSimpleModelNodeVO 节点
BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
&& evalConditionExpress(variables, conditionNode.getConditionSetting()));
conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
&& evalConditionExpress(variables, conditionNode.getConditionSetting()));
if (matchConditionNode == null) {
matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(),
conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()));
@@ -974,8 +977,8 @@ public class SimpleModelUtils {
if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) {
// 查找满足条件的 BpmSimpleModelNodeVO 节点
Collection<BpmSimpleModelNodeVO> matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
&& evalConditionExpress(variables, conditionNode.getConditionSetting()));
conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())
&& evalConditionExpress(variables, conditionNode.getConditionSetting()));
if (CollUtil.isEmpty(matchConditionNodes)) {
matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(),
conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()));

View File

@@ -387,8 +387,6 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) {
// 遍历 tasks 列表,只处理已结束的 UserTask
// 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks没有 activities导致如果遍历 activities 的话,它无法成为一个节点
// TODO @芋艿子流程只有activity这里获取不到已结束的子流程
// TODO @lesan【子流程】基于 activities 查询出 usertask、callactivity然后拼接如果是子流程就是可以点击过去
List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null);
List<ActivityNode> approvalNodes = convertList(endTasks, task -> {
FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
@@ -410,7 +408,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 遍历 activities只处理已结束的 StartEvent、EndEvent
List<HistoricActivityInstance> endActivities = filterList(activities, activity -> activity.getEndTime() != null
&& (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_EVENT_START, ELEMENT_EVENT_END)));
&& (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_EVENT_START, ELEMENT_CALL_ACTIVITY, ELEMENT_EVENT_END)));
endActivities.forEach(activity -> {
// StartEvent只处理 BPMN 的场景。因为SIMPLE 情况下,已经有 START_USER_NODE 节点
if (ELEMENT_EVENT_START.equals(activity.getActivityType())
@@ -444,7 +442,20 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
}
approvalNodes.add(endNode);
}
// CallActivity
if (ELEMENT_CALL_ACTIVITY.equals(activity.getActivityType())) {
ActivityNode callActivity = new ActivityNode().setId(activity.getId())
.setName(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getName())
.setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus)
.setStartTime(DateUtils.of(activity.getStartTime()))
.setEndTime(DateUtils.of(activity.getEndTime()))
.setProcessInstanceId(activity.getProcessInstanceId());
approvalNodes.add(callActivity);
}
});
// 按照时间排序
approvalNodes.sort(Comparator.comparing(ActivityNode::getStartTime));
return approvalNodes;
}
@@ -464,7 +475,6 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
HistoricActivityInstance::getActivityId);
// 按照 activityId 分组,构建 ApprovalNodeInfo 节点
// TODO @lesan【子流程】在子流程进行审批的时候HistoricActivityInstance 里面可以拿到 runActivities.get(0).getCalledProcessInstanceId()。要不要支持跳转???
Map<String, HistoricTaskInstance> taskMap = convertMap(tasks, HistoricTaskInstance::getId);
return convertList(runningTaskMap.entrySet(), entry -> {
String activityId = entry.getKey();
@@ -510,6 +520,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
approvalTaskInfo.getAssignee())); // 委派或者向前加签情况,需要先比较 owner
activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size()));
}
if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) {
activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId());
}
return activityNode;
});
}
@@ -823,6 +836,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
&& Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) {
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW);
}
// 1.4 子流程不允许取消
if (StrUtil.isNotBlank(instance.getSuperExecutionId())) {
throw exception(PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW);
}
// 2. 取消流程
updateProcessInstanceCancel(cancelReqVO.getId(),
@@ -849,7 +866,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
BpmProcessInstanceStatusEnum.CANCEL.getStatus());
runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason);
// 2. 结束流程
// 2. 取消所有子流程
List<ProcessInstance> childProcessInstances = runtimeService.createProcessInstanceQuery()
.superProcessInstanceId(id).list();
childProcessInstances.forEach(processInstance -> updateProcessInstanceCancel(
processInstance.getProcessInstanceId(), BpmReasonEnum.CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS.getReason()));
// 3. 结束流程
taskService.moveTaskToEnd(id, reason);
}

View File

@@ -13,7 +13,6 @@ import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig;

View File

@@ -12,9 +12,10 @@ import lombok.Getter;
@Getter
public enum CodegenFrontTypeEnum {
VUE2(10), // Vue2 Element UI 标准模版
VUE3(20), // Vue3 Element Plus 标准模版
VUE3_VBEN(30), // Vue3 VBEN 模版
VUE2_ELEMENT_UI(10), // Vue2 Element UI 标准模版
VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版
VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版
VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
;
/**

View File

@@ -101,49 +101,68 @@ public class CodegenEngine {
* value生成的路径
*/
private static final Table<Integer, String, String> FRONT_TEMPLATES = ImmutableTable.<Integer, String, String>builder()
// Vue2 标准模版
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/index.vue"),
// VUE2_ELEMENT_UI
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/index.vue"),
vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"),
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("api/api.js"),
vueFilePath("api/${table.moduleName}/${table.businessName}/index.js"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/form.vue"),
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/form.vue"),
vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType(), vueTemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
// Vue3 标准模版
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"),
// VUE3_ELEMENT_PLUS
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/index.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/form.vue"),
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/form.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_erp.vue"), // 特殊:主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/list_sub_inner.vue"), // 特殊:主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/list_sub_erp.vue"), // 特殊:主子表专属逻辑
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"),
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
// Vue3 vben 模版
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
// VUE3_VBEN2_ANTD_SCHEMA
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/index.vue"),
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/index.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/form.vue"),
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/form.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("api/api.ts"),
.put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
// VUE3_VBEN5_ANTD_SCHEMA
// TODO @puhui999目录改成 vue3_vben5_antd然后里面有 schema目前我们在写的和 general你微信里提的原生的感觉也要搞
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/data.ts"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/index.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/form.vue"),
vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("api/api.ts"),
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
// 主子表模板配置 - Vue3 vben5 schema 模版
//.put(CodegenFrontTypeEnum.VUE3_VBEN_NEXT_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/master_slave_data.ts"),
// vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts"))
//.put(CodegenFrontTypeEnum.VUE3_VBEN_NEXT_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/master_slave_index.vue"),
// vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
//.put(CodegenFrontTypeEnum.VUE3_VBEN_NEXT_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/modules/master_slave_form.vue"),
// vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
//.put(CodegenFrontTypeEnum.VUE3_VBEN_NEXT_SCHEMA.getType(), vue3VbenNextSchemaTemplatePath("views/modules/sub_table.vue"),
// vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/sub_table.vue"))
.build();
@Resource
@@ -496,6 +515,10 @@ public class CodegenEngine {
return "codegen/vue3_vben/" + path + ".vm";
}
private static String vue3VbenNextSchemaTemplatePath(String path) {
return "codegen/vue3_vben_next/schema/" + path + ".vm";
}
private static boolean isSubTemplate(String path) {
return path.contains("_sub");
}

View File

@@ -0,0 +1,118 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
export namespace ${simpleClassName}Api {
/** ${table.classComment}信息 */
export interface ${simpleClassName} {
#foreach ($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: Date; // ${column.columnComment}
#else
${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
#end
#end
#end
#if ( $table.templateType == 2 )
children?: ${simpleClassName}[];
#end
}
}
#if ( $table.templateType != 2 )
/** 查询${table.classComment}分页 */
export function get${simpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
}
#else
/** 查询${table.classComment}列表 */
export function get${simpleClassName}List(params: any) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
}
#end
/** 查询${table.classComment}详情 */
export function get${simpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
}
/** 新增${table.classComment} */
export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.post('${baseURL}/create', data);
}
/** 修改${table.classComment} */
export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.put('${baseURL}/update', data);
}
/** 删除${table.classComment} */
export function delete${simpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/delete?id=${id}`);
}
/** 导出${table.classComment} */
export function export${simpleClassName}(params: any) {
return requestClient.download('${baseURL}/export-excel', params);
}
## 特殊:主子表专属逻辑
#foreach ($subTable in $subTables)
#set ($index = $foreach.count - 1)
#set ($subSimpleClassName = $subSimpleClassNames.get($index))
#set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
#set ($subClassNameVar = $subClassNameVars.get($index))
// ==================== 子表($subTable.classComment ====================
## 情况一MASTER_ERP 时,需要分查询页子表
#if ( $table.templateType == 11 )
/** 获得${subTable.classComment}分页 */
export function get${subSimpleClassName}Page(params: PageParam) {
return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
}
## 情况二:非 MASTER_ERP 时,需要列表查询子表
#else
#if ( $subTable.subJoinMany )
/** 获得${subTable.classComment}列表 */
export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#else
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
}
#end
#end
## 特殊MASTER_ERP 时,支持单个的新增、修改、删除操作
#if ( $table.templateType == 11 )
/** 新增${subTable.classComment} */
export function create${subSimpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
}
/** 修改${subTable.classComment} */
export function update${subSimpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
}
/** 删除${subTable.classComment} */
export function delete${subSimpleClassName}(id: number) {
return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
}
/** 获得${subTable.classComment} */
export function get${subSimpleClassName}(id: number) {
return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
}
#end
#end

View File

@@ -0,0 +1,276 @@
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import type { VbenFormSchema } from '#/adapter/form';
import type { OnActionClickFn } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { z } from '#/adapter/form';
#if(${table.templateType} == 2)## 树表需要导入这些
import { get${simpleClassName}List } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { handleTree } from '#/utils/tree';
#end
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { useAccess } from '@vben/access';
const { hasAccessByCodes } = useAccess();
/** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'id',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
#if(${table.templateType} == 2)## 树表特有字段:上级
{
fieldName: '${treeParentColumn.javaField}',
label: '上级${table.classComment}',
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: async () => {
const data = await get${simpleClassName}List({});
data.unshift({
id: 0,
${treeNameColumn.javaField}: '顶级${table.classComment}',
});
return handleTree(data);
},
class: 'w-full',
labelField: '${treeNameColumn.javaField}',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择上级${table.classComment}',
treeDefaultExpandAll: true,
},
rules: 'selectRequired',
},
#end
#foreach($column in $columns)
#if ($column.createOperation || $column.updateOperation)
#if (!$column.primaryKey && ($table.templateType != 2 || ($table.templateType == 2 && $column.id != $treeParentColumn.id)))## 树表中已经添加了父ID字段这里排除
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "number")
#elseif ($javaType == "String")
#set ($dictMethod = "string")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "boolean")
#end
{
fieldName: '${javaField}',
label: '${comment}',
#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
rules: 'required',
#end
#if ($column.htmlType == "input")
component: 'Input',
componentProps: {
placeholder: '请输入${comment}',
},
#elseif($column.htmlType == "imageUpload")## 图片上传
component: 'FileUpload',
componentProps: {
fileType: 'image',
maxCount: 1,
},
#elseif($column.htmlType == "fileUpload")## 文件上传
component: 'FileUpload',
componentProps: {
fileType: 'file',
maxCount: 1,
},
#elseif($column.htmlType == "editor")## 文本编辑器
component: 'Editor',
#elseif($column.htmlType == "select")## 下拉框
component: 'Select',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options: [],
#end
placeholder: '请选择${comment}',
class: 'w-full',
},
#elseif($column.htmlType == "checkbox")## 多选框
component: 'Checkbox',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options: [],
#end
},
#elseif($column.htmlType == "radio")## 单选框
component: 'RadioGroup',
componentProps: {
#if ("" != $dictType)## 有数据字典
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else##没数据字典
options: [],
#end
buttonStyle: 'solid',
optionType: 'button',
},
#elseif($column.htmlType == "datetime")## 时间框
component: 'DatePicker',
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
},
#elseif($column.htmlType == "textarea")## 文本域
component: 'Textarea',
componentProps: {
placeholder: '请输入${comment}',
},
#elseif($column.htmlType == "inputNumber")## 数字输入框
component: 'InputNumber',
componentProps: {
min: 0,
class: 'w-full',
controlsPosition: 'right',
placeholder: '请输入${comment}',
},
#end
},
#end
#end
#end
];
}
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
#foreach($column in $columns)
#if ($column.listOperation)
#set ($dictType = $column.dictType)
#set ($javaType = $column.javaType)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
#set ($dictMethod = "number")
#elseif ($javaType == "String")
#set ($dictMethod = "string")
#elseif ($javaType == "Boolean")
#set ($dictMethod = "boolean")
#end
{
fieldName: '${javaField}',
label: '${comment}',
#if ($column.htmlType == "input")
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入${comment}',
},
#elseif ($column.htmlType == "select")
component: 'Select',
componentProps: {
allowClear: true,
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
options: [],
#end
placeholder: '请选择${comment}',
},
#elseif ($column.htmlType == "radio")
component: 'Select',
componentProps: {
allowClear: true,
#if ("" != $dictType)## 设置了 dictType 数据字典的情况
options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), '$dictMethod'),
#else## 未设置 dictType 数据字典的情况
options: [],
#end
},
#elseif($column.htmlType == "datetime")
component: 'RangePicker',
componentProps: {
allowClear: true,
},
#end
},
#end
#end
];
}
/** 列表的字段 */
export function useGridColumns(
onActionClick?: OnActionClickFn<${simpleClassName}Api.${simpleClassName}>,
): VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>['columns'] {
return [
#foreach($column in $columns)
#if ($column.listOperationResult)
#set ($dictType = $column.dictType)
#set ($javaField = $column.javaField)
#set ($comment = $column.columnComment)
{
field: '${javaField}',
title: '${comment}',
minWidth: 120,
#if ($column.javaType == "LocalDateTime")## 时间类型
formatter: 'formatDateTime',
#elseif("" != $dictType)## 数据字典
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.$dictType.toUpperCase() },
},
#end
#if (${table.templateType} == 2 && $column.id == $treeNameColumn.id)## 树表特有:标记树节点列
treeNode: true,
#end
},
#end
#end
{
field: 'operation',
title: '操作',
minWidth: 200,
align: 'right',
fixed: 'right',
headerAlign: 'center',
showOverflow: false,
cellRender: {
attrs: {
nameField: '${columns[0].javaField}',
nameTitle: '${table.classComment}',
onClick: onActionClick,
},
name: 'CellOperation',
options: [
#if (${table.templateType} == 2)## 树表特有操作
{
code: 'add_child',
text: '新增下级',
show: hasAccessByCodes(['${table.moduleName}:${simpleClassName_strikeCase}:create']),
},
#end
{
code: 'edit',
show: hasAccessByCodes(['${table.moduleName}:${simpleClassName_strikeCase}:update']),
},
{
code: 'delete',
show: hasAccessByCodes(['${table.moduleName}:${simpleClassName_strikeCase}:delete']),
#if (${table.templateType} == 2)## 树表禁止删除带有子节点的数据
disabled: (row: ${simpleClassName}Api.${simpleClassName}) => {
return !!(row.children && row.children.length > 0);
},
#end
},
],
},
},
];
}

View File

@@ -0,0 +1,118 @@
<script lang="ts" setup>
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { computed, ref } from 'vue';
import { $t } from '#/locales';
import { useVbenForm } from '#/adapter/form';
import { get${simpleClassName}, create${simpleClassName}, update${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formData = ref<${simpleClassName}Api.${simpleClassName}>();
#if (${table.templateType} == 2)## 树表特有父ID处理
const parentId = ref<number>(); // 新增下级时的父级 ID
const getTitle = computed(() => {
if (formData.value?.id) {
return $t('ui.actionTitle.edit', ['${table.classComment}']);
}
return parentId.value
? $t('ui.actionTitle.create', ['下级${table.classComment}'])
: $t('ui.actionTitle.create', ['${table.classComment}']);
});
#else## 标准表标题
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['${table.classComment}'])
: $t('ui.actionTitle.create', ['${table.classComment}']);
});
#end
const [Form, formApi] = useVbenForm({
layout: 'horizontal',
schema: useFormSchema(),
showDefaultActions: false
});
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data = (await formApi.getValues()) as ${simpleClassName}Api.${simpleClassName};
try {
await (formData.value?.id ? update${simpleClassName}(data) : create${simpleClassName}(data));
// 关闭并提示
await modalApi.close();
emit('success');
message.success({
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
} finally {
modalApi.lock(false);
}
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
return;
}
// 加载数据
#if (${table.templateType} == 2)## 树表处理传入的父ID
let data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();
#else## 标准表直接获取
const data = modalApi.getData<${simpleClassName}Api.${simpleClassName}>();
#end
if (!data) {
return;
}
#if (${table.templateType} == 2)## 树表特有:处理新增下级的情况
// 处理新增下级的情况
if (!data.id && data.${treeParentColumn.javaField}) {
parentId.value = data.${treeParentColumn.javaField};
formData.value = { ${treeParentColumn.javaField}: parentId.value } as ${simpleClassName}Api.${simpleClassName};
await formApi.setValues(formData.value);
return;
}
#end
if (data.id) {
// 编辑
modalApi.lock();
try {
#if (${table.templateType} == 2)## 树表获取数据后重新赋值
data = await get${simpleClassName}(data.id);
formData.value = data;
#else## 标准表设置表单数据
formData.value = await get${simpleClassName}(data.id as number);
#end
await formApi.setValues(formData.value);
} finally {
modalApi.lock(false);
}
} else {
// 新增
#if (${table.templateType} == 2)## 树表特有设置顶级ID
formData.value = { ${treeParentColumn.javaField}: 0 } as ${simpleClassName}Api.${simpleClassName};
#else## 标准表:设置空值
formData.value = data;
#end
await formApi.setValues(formData.value || {});
}
},
});
</script>
<template>
<Modal :title="getTitle">
<Form class="mx-4" />
</Modal>
</template>

View File

@@ -0,0 +1,181 @@
<script lang="ts" setup>
import type { OnActionClickParams, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ${simpleClassName}Api } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
import { Page, useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue';
import { Download, Plus } from '@vben/icons';
import Form from './modules/form.vue';
import { ref } from 'vue';
import { $t } from '#/locales';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
#if (${table.templateType} == 2)## 树表接口
import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#else## 标准表接口
import { get${simpleClassName}Page, delete${simpleClassName}, export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
#end
import { downloadByData } from '#/utils/download';
import { useGridColumns, useGridFormSchema } from './data';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
/** 切换树形展开/收缩状态 */
const isExpanded = ref(true);
function toggleExpand() {
isExpanded.value = !isExpanded.value;
gridApi.grid.setAllTreeExpand(isExpanded.value);
}
#end
/** 刷新表格 */
function onRefresh() {
gridApi.query();
}
/** 导出表格 */
async function onExport() {
const data = await export${simpleClassName}(await gridApi.formApi.getValues());
downloadByData(data, '${table.classComment}.xls');
}
/** 创建${table.classComment} */
function onCreate() {
formModalApi.setData(null).open();
}
/** 编辑${table.classComment} */
function onEdit(row: ${simpleClassName}Api.${simpleClassName}) {
formModalApi.setData(row).open();
}
#if (${table.templateType} == 2)## 树表特有:新增下级
/** 新增下级${table.classComment} */
function onAddChild(row: ${simpleClassName}Api.${simpleClassName}) {
formModalApi.setData({ ${treeParentColumn.javaField}: row.id }).open();
}
#end
/** 删除${table.classComment} */
async function onDelete(row: ${simpleClassName}Api.${simpleClassName}) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.id]),
duration: 0,
key: 'action_process_msg',
});
try {
await delete${simpleClassName}(row.id as number);
message.success({
content: $t('ui.actionMessage.deleteSuccess', [row.id]),
key: 'action_process_msg',
});
onRefresh();
} catch {
hideLoading();
}
}
/** 表格操作按钮的回调函数 */
function onActionClick({
code,
row,
}: OnActionClickParams<${simpleClassName}Api.${simpleClassName}>) {
switch (code) {
case 'edit': {
onEdit(row);
break;
}
case 'delete': {
onDelete(row);
break;
}
#if (${table.templateType} == 2)## 树表特有:新增下级
case 'add_child': {
onAddChild(row);
break;
}
#end
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(onActionClick),
height: 'auto',
#if (${table.templateType} == 2)## 树表设置
treeConfig: {
parentField: '${treeParentColumn.javaField}',
rowField: 'id',
transform: true,
expandAll: true,
reserve: true,
},
pagerConfig: {
enabled: false,
},
#else## 标准表设置
pagerConfig: {
enabled: true,
},
#end
proxyConfig: {
ajax: {
#if (${table.templateType} == 2)## 树表数据加载
query: async (_, formValues) => {
return await get${simpleClassName}List(formValues);
},
#else## 标准表数据加载
query: async ({ page }, formValues) => {
const { items, total } = await get${simpleClassName}Page({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
return { items, total };
},
#end
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: { code: 'query' },
search: true,
},
} as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>,
});
</script>
<template>
<Page auto-content-height>
<FormModal @success="onRefresh" />
<Grid table-title="${table.classComment}列表">
<template #toolbar-tools>
#if (${table.templateType} == 2)## 树表特有:展开/收缩按钮
<Button @click="toggleExpand" class="mr-2">
{{ isExpanded ? '收缩' : '展开' }}
</Button>
#end
<Button type="primary" @click="onCreate" v-access:code="['${table.moduleName}:${simpleClassName_strikeCase}:create']">
<Plus class="size-5" />
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
</Button>
<Button type="primary" class="ml-2" @click="onExport" v-access:code="['${table.moduleName}:${simpleClassName_strikeCase}:export']">
<Download class="size-5" />
{{ $t('ui.actionTitle.export') }}
</Button>
</template>
</Grid>
</Page>
</template>

View File

@@ -105,7 +105,7 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
when(codegenBuilder.buildColumns(eq(table.getId()), same(fields)))
.thenReturn(columns);
// mock 方法CodegenProperties
when(codegenProperties.getFrontType()).thenReturn(CodegenFrontTypeEnum.VUE3.getType());
when(codegenProperties.getFrontType()).thenReturn(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType());
// 调用
List<Long> result = codegenService.createCodegenList(userId, reqVO);
@@ -116,7 +116,7 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(table, dbTable);
assertEquals(1L, dbTable.getDataSourceConfigId());
assertEquals(CodegenSceneEnum.ADMIN.getScene(), dbTable.getScene());
assertEquals(CodegenFrontTypeEnum.VUE3.getType(), dbTable.getFrontType());
assertEquals(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), dbTable.getFrontType());
assertEquals("芋头", dbTable.getAuthor());
// 断言CodegenColumnDO
List<CodegenColumnDO> dbColumns = codegenColumnMapper.selectList();

View File

@@ -23,7 +23,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
public void testExecute_vue2_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE2.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
@@ -39,7 +39,7 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
public void testExecute_vue2_tree() {
// 准备参数
CodegenTableDO table = getTable("category")
.setFrontType(CodegenFrontTypeEnum.VUE2.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
List<CodegenColumnDO> columns = getColumnList("category");
@@ -71,19 +71,19 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
String path) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE2.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2.getType())
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");

View File

@@ -23,7 +23,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
public void testExecute_vue3_one() {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE3.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
List<CodegenColumnDO> columns = getColumnList("student");
@@ -39,7 +39,7 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
public void testExecute_vue3_tree() {
// 准备参数
CodegenTableDO table = getTable("category")
.setFrontType(CodegenFrontTypeEnum.VUE3.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
List<CodegenColumnDO> columns = getColumnList("category");
@@ -71,19 +71,19 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
String path) {
// 准备参数
CodegenTableDO table = getTable("student")
.setFrontType(CodegenFrontTypeEnum.VUE3.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setTemplateType(templateType.getType());
List<CodegenColumnDO> columns = getColumnList("student");
// 准备参数(子表)
CodegenTableDO contactTable = getTable("contact")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setSubJoinColumnId(100L).setSubJoinMany(true);
List<CodegenColumnDO> contactColumns = getColumnList("contact");
// 准备参数(班主任)
CodegenTableDO teacherTable = getTable("teacher")
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3.getType())
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
.setSubJoinColumnId(200L).setSubJoinMany(false);
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");

View File

@@ -1,42 +0,0 @@
package cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "用户 App - 客服消息 Response VO")
@Data
public class AppKeFuMessageRespVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23202")
private Long id;
@Schema(description = "会话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12580")
private Long conversationId;
@Schema(description = "发送人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24571")
private Long senderId;
@Schema(description = "发送人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer senderType;
@Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "29124")
private Long receiverId;
@Schema(description = "接收人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
private Integer receiverType;
@Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer contentType;
@Schema(description = "消息", requiredMode = Schema.RequiredMode.REQUIRED)
private String content;
@Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Boolean readStatus;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
}

View File

@@ -7,7 +7,9 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageListReqVO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageRespVO;
import cn.iocoder.yudao.module.promotion.controller.admin.kefu.vo.message.KeFuMessageSendReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessagePageReqVO;
import cn.iocoder.yudao.module.promotion.controller.app.kefu.vo.message.AppKeFuMessageSendReqVO;
@@ -15,6 +17,7 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuConversationDO;
import cn.iocoder.yudao.module.promotion.dal.dataobject.kefu.KeFuMessageDO;
import cn.iocoder.yudao.module.promotion.dal.mysql.kefu.KeFuMessageMapper;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -66,9 +69,11 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
conversationService.updateConversationLastMessage(kefuMessage);
// 3.1 发送消息给会员
getSelf().sendAsyncMessageToMember(conversation.getUserId(), KEFU_MESSAGE_TYPE, kefuMessage);
AdminUserRespDTO user = adminUserApi.getUser(kefuMessage.getSenderId());
KeFuMessageRespVO message = BeanUtils.toBean(kefuMessage, KeFuMessageRespVO.class).setSenderAvatar(user.getAvatar());
getSelf().sendAsyncMessageToMember(conversation.getUserId(), KEFU_MESSAGE_TYPE, message);
// 3.2 通知所有管理员更新对话
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_TYPE, kefuMessage);
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_TYPE, message);
return kefuMessage.getId();
}
@@ -84,7 +89,9 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
// 2. 更新会话消息冗余
conversationService.updateConversationLastMessage(kefuMessage);
// 3. 通知所有管理员更新对话
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_TYPE, kefuMessage);
MemberUserRespDTO user = memberUserApi.getUser(kefuMessage.getSenderId());
KeFuMessageRespVO message = BeanUtils.toBean(kefuMessage, KeFuMessageRespVO.class).setSenderAvatar(user.getAvatar());
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_TYPE, message);
return kefuMessage.getId();
}
@@ -112,9 +119,11 @@ public class KeFuMessageServiceImpl implements KeFuMessageService {
// 2.3 发送消息通知会员,管理员已读 -> 会员更新发送的消息状态
KeFuMessageDO keFuMessage = getFirst(filterList(messageList, message -> UserTypeEnum.MEMBER.getValue().equals(message.getSenderType())));
assert keFuMessage != null; // 断言避免警告
getSelf().sendAsyncMessageToMember(keFuMessage.getSenderId(), KEFU_MESSAGE_ADMIN_READ, conversation.getId());
getSelf().sendAsyncMessageToMember(keFuMessage.getSenderId(), KEFU_MESSAGE_ADMIN_READ,
new KeFuMessageRespVO().setConversationId(keFuMessage.getConversationId()));
// 2.4 通知所有管理员消息已读
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_ADMIN_READ, conversation.getId());
getSelf().sendAsyncMessageToAdmin(KEFU_MESSAGE_ADMIN_READ,
new KeFuMessageRespVO().setConversationId(keFuMessage.getConversationId()));
}
private void validateReceiverExist(Long receiverId, Integer receiverType) {

View File

@@ -213,7 +213,7 @@ public class TradeDeliveryPriceCalculator implements TradePriceCalculator {
}
double totalChargeValue = getTotalChargeValue(orderItems, chargeMode);
double totalPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems);
return totalChargeValue >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice();
return totalChargeValue <= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice();
}
private double getTotalChargeValue(List<OrderItem> orderItems, Integer chargeMode) {

View File

@@ -212,6 +212,10 @@ yudao:
appKey: 75b161ed2aef4719b275d6e7f2a4d4cd
secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz
model: generalv3.5
baichuan: # 百川智能
enable: true
api-key: sk-abc
model: Baichuan4-Turbo
midjourney:
enable: true
# base-url: https://api.holdai.top/mj-relax/mj