feat: AI工作流

This commit is contained in:
Lesan 2025-03-24 15:15:27 +08:00
parent 138239324c
commit 587504c36a
12 changed files with 504 additions and 0 deletions

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_KEY_EXISTS = new ErrorCode(1_040_011_001, "工作流标识已存在");
}

View File

@ -0,0 +1,85 @@
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));
}
@PutMapping("/updateWorkflowModel")
@Operation(summary = "更新 AI 工作流模型")
@PreAuthorize("@ss.hasPermission('ai:workflow:update')")
public CommonResult<Boolean> updateWorkflowModel(@Valid @RequestBody AiWorkflowUpdateModelReqVO updateReqVO) {
workflowService.updateWorkflowModel(updateReqVO);
return success(true);
}
@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,26 @@
package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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 definitionKey;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -0,0 +1,27 @@
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 definitionKey;
@Schema(description = "工作流名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流")
private String name;
@Schema(description = "工作流模型 JSON", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private String model;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,22 @@
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;
@Schema(description = "管理后台 - AI 工作流新增/修改 Request VO")
@Data
public class AiWorkflowSaveReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "工作流标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "FLOW")
@NotEmpty(message = "工作流标识不能为空")
private String definitionKey;
@Schema(description = "工作流名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "工作流")
@NotEmpty(message = "工作流名称不能为空")
private String name;
}

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 model;
@Schema(description = "参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private Map<String, Object> params;
}

View File

@ -0,0 +1,18 @@
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;
@Schema(description = "管理后台 - AI 工作流修改流程模型 Request VO")
@Data
public class AiWorkflowUpdateModelReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "工作流模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
@NotEmpty(message = "工作流模型不能为空")
private String model;
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.workflow;
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") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
public class AiWorkflowDO extends BaseDO {
/**
* 编号
*/
@TableId
private Long id;
/**
* 工作流标识
*/
private String definitionKey;
/**
* 工作流名称
*/
private String name;
/**
* 工作流模型 JSON 数据
*/
private String model;
}

View File

@ -0,0 +1,28 @@
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 selectByKey(String key) {
return selectOne(AiWorkflowDO::getDefinitionKey, key);
}
default PageResult<AiWorkflowDO> selectPage(AiWorkflowPageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiWorkflowDO>()
.likeIfPresent(AiWorkflowDO::getName, pageReqVO.getName())
.likeIfPresent(AiWorkflowDO::getDefinitionKey, pageReqVO.getDefinitionKey()));
}
}

View File

@ -0,0 +1,69 @@
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.controller.admin.workflow.vo.AiWorkflowUpdateModelReqVO;
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);
/**
* 修改工作流模型 JSON 数据
*
* @param updateReqVO 更新数据
*/
void updateWorkflowModel(AiWorkflowUpdateModelReqVO updateReqVO);
/**
* 测试 AI 工作流
*
* @param testReqVO 测试数据
*/
Object testWorkflow(AiWorkflowTestReqVO testReqVO);
}

View File

@ -0,0 +1,157 @@
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.controller.admin.workflow.vo.AiWorkflowUpdateModelReqVO;
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_KEY_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.getDefinitionKey());
AiWorkflowDO workflow = BeanUtils.toBean(createReqVO, AiWorkflowDO.class);
workflow.setModel(StrUtil.EMPTY);
workflowMapper.insert(workflow);
return workflow.getId();
}
@Override
public void updateWorkflow(AiWorkflowSaveReqVO updateReqVO) {
validateWorkflowForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getDefinitionKey());
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 void updateWorkflowModel(AiWorkflowUpdateModelReqVO updateReqVO) {
validateWorkflowExists(updateReqVO.getId());
workflowMapper.updateById(new AiWorkflowDO().setId(updateReqVO.getId()).setModel(updateReqVO.getModel()));
}
@Override
public Object testWorkflow(AiWorkflowTestReqVO testReqVO) {
Map<String, Object> variables = testReqVO.getParams();
Tinyflow tinyflow = parseFlowParam(testReqVO.getModel());
return tinyflow.toChain().executeForResult(variables);
}
private void validateWorkflowForCreateOrUpdate(Long id, String key) {
validateWorkflowExists(id);
validateKeyUnique(id, key);
}
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 validateKeyUnique(Long id, String key) {
if (StrUtil.isBlank(key)) {
return;
}
AiWorkflowDO workflow = workflowMapper.selectByKey(key);
if (ObjUtil.isNull(workflow)) {
return;
}
if (ObjUtil.isNull(id)) {
throw exception(WORKFLOW_KEY_EXISTS);
}
if (ObjUtil.notEqual(workflow.getId(), id)) {
throw exception(WORKFLOW_KEY_EXISTS);
}
}
private Tinyflow parseFlowParam(String model) {
JSONObject json = JSONObject.parseObject(model);
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 需要讨论一下这里怎么弄
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,13 @@
</exclusions>
</dependency>
<dependency>
<!-- Tinyflow工作流 -->
<groupId>dev.tinyflow</groupId>
<artifactId>tinyflow-java-core</artifactId>
<version>${tinyflow.version}</version>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>