feat: streamline workflow orchestration and chat routing

Add Zhipu web search integration, remove obsolete workflow nodes and resume handling, and separate model, agent, and workflow chat execution.
This commit is contained in:
ageerle
2026-07-30 00:15:44 +08:00
parent 83fd1ee983
commit 6e264ad500
45 changed files with 1217 additions and 1580 deletions

View File

@@ -1,16 +1,13 @@
package org.ruoyi.workflow.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import org.ruoyi.common.core.domain.R;
import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto;
import org.ruoyi.workflow.dto.workflow.WfRuntimeResp;
import org.ruoyi.workflow.dto.workflow.WorkflowResumeReq;
import org.ruoyi.workflow.service.WorkflowRuntimeService;
import org.ruoyi.workflow.workflow.WorkflowStarter;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -24,16 +21,6 @@ public class WorkflowRuntimeController {
@Resource
private WorkflowRuntimeService workflowRuntimeService;
@Resource
private WorkflowStarter workflowStarter;
@Operation(summary = "接收用户输入以继续执行剩余流程")
@PostMapping(value = "/resume/{runtimeUuid}")
public R resume(@PathVariable String runtimeUuid, @RequestBody WorkflowResumeReq resumeReq) {
workflowStarter.resumeFlow(runtimeUuid, resumeReq.getFeedbackContent(), resumeReq.getSseEmitter());
return R.ok();
}
@GetMapping("/page")
public R<Page<WfRuntimeResp>> search(@RequestParam String wfUuid,
@NotNull @Min(1) Integer currentPage,

View File

@@ -337,7 +337,6 @@ public class AdiConstant {
public static final String DEFAULT_INPUT_PARAM_NAME = "input";
public static final String DEFAULT_OUTPUT_PARAM_NAME = "output";
public static final String DEFAULT_ERROR_OUTPUT_PARAM_NAME = "error_msg";
public static final String HUMAN_FEEDBACK_KEY = "human_feedback";
public static final int NODE_PROCESS_STATUS_READY = 1;
public static final int NODE_PROCESS_STATUS_DOING = 2;
public static final int NODE_PROCESS_STATUS_SUCCESS = 3;
@@ -347,7 +346,6 @@ public class AdiConstant {
public static final int WORKFLOW_PROCESS_STATUS_DOING = 2;
public static final int WORKFLOW_PROCESS_STATUS_SUCCESS = 3;
public static final int WORKFLOW_PROCESS_STATUS_FAIL = 4;
public static final int WORKFLOW_PROCESS_STATUS_WAITING_INPUT = 5;
public static final int WORKFLOW_NODE_PROCESS_TYPE_NORMAL = 1;
public static final int WORKFLOW_NODE_PROCESS_TYPE_CONDITIONAL = 2;

View File

@@ -1,10 +0,0 @@
package org.ruoyi.workflow.dto.workflow;
import lombok.Data;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@Data
public class WorkflowResumeReq {
private String feedbackContent;
private SseEmitter sseEmitter;
}

View File

@@ -55,8 +55,13 @@ public class SSEEmitterHelper {
} else {
sseEmitter.send(msg);
}
} catch (IllegalStateException ise) {
// SSE连接已关闭用户刷新页面、关闭标签页或重新提交
log.warn("SSE emitter already completed for event [{}], ignoring", name);
COMPLETED_SSE.put(sseEmitter, Boolean.TRUE);
} catch (IOException ioException) {
log.error("stream onNext error", ioException);
COMPLETED_SSE.put(sseEmitter, Boolean.TRUE);
}
}

View File

@@ -4,7 +4,6 @@ import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.enums.RoleType;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.core.exception.ServiceException;
import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.common.core.utils.StringUtils;
@@ -12,6 +11,7 @@ import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.helper.SSEEmitterHelper;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.WorkflowUtil;
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
@@ -38,17 +38,20 @@ public class WorkflowMessageUtil {
/**
* 获取节点的响应模板
* 获取节点的响应模板 <br/>
* 优先读取 sys_config 配置(可在系统管理-配置管理中自定义),
* 未配置时回退到枚举内置默认模板, 模板仅为展示文案, 缺失不应中断工作流执行
* @param configKey 参数Key
* @return 返回模板样式
*/
public static String getNodeMessageTemplate(String configKey){
ConfigService configService = SpringUtil.getBean(ConfigService.class);
String configValue = configService.getConfigValue(configKey);
if (StringUtils.isEmpty(configValue)) {
throw new ServiceException("请先配置该节点的响应模板");
if (StringUtils.isNotEmpty(configValue)) {
return configValue;
}
return configValue;
log.warn("sys_config 未配置节点响应模板 [{}], 已回退使用内置默认模板", configKey);
return NodeMessageTemplateEnum.getDefaultTemplate(configKey);
}
/**

View File

@@ -1,17 +0,0 @@
package org.ruoyi.workflow.workflow;
import org.apache.commons.collections4.map.PassiveExpiringMap;
/**
* 已中断正在等待用户输入的流程 <br/>
* TODO 需要考虑项目多节点部署的情况
*/
public class InterruptedFlow {
/**
* 10分钟超时
*/
private static final PassiveExpiringMap.ExpirationPolicy<String, WorkflowEngine> ep = new PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(60 * 1000 * 10);
public static PassiveExpiringMap<String, WorkflowEngine> RUNTIME_TO_GRAPH = new PassiveExpiringMap<>(ep);
}

View File

@@ -16,24 +16,14 @@ public enum WfComponentNameEnum {
TONGYI_WANX("Tongyiwanx"),
DOCUMENT_EXTRACTOR("DocumentExtractor"),
KEYWORD_EXTRACTOR("KeywordExtractor"),
FAQ_EXTRACTOR("FaqExtractor"),
KNOWLEDGE_RETRIEVER("KnowledgeRetrieval"),
SWITCHER("Switcher"),
CLASSIFIER("Classifier"),
TEMPLATE("Template"),
GOOGLE_SEARCH("Google"),
HUMAN_FEEDBACK("HumanFeedback"),
MAIL_SEND("MailSend"),
HTTP_REQUEST("HttpRequest");

View File

@@ -4,15 +4,14 @@ import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.ruoyi.workflow.workflow.node.EndNode;
import org.ruoyi.workflow.workflow.node.humanFeedBack.HumanFeedbackNode;
import org.ruoyi.workflow.workflow.node.answer.LLMAnswerNode;
import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode;
import org.ruoyi.workflow.workflow.node.image.ImageNode;
import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode;
import org.ruoyi.workflow.workflow.node.knowledgeRetrieval.KnowledgeRetrievalNode;
import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode;
import org.ruoyi.workflow.workflow.node.start.StartNode;
import org.ruoyi.workflow.workflow.node.switcher.SwitcherNode;
import org.ruoyi.workflow.workflow.node.googleSearch.GoogleSearchNode;
public class WfNodeFactory {
public static AbstractWfNode create(WorkflowComponent wfComponent, WorkflowNode nodeDefinition,
@@ -21,14 +20,13 @@ public class WfNodeFactory {
switch (WfComponentNameEnum.getByName(wfComponent.getName())) {
case START -> wfNode = new StartNode(wfComponent, nodeDefinition, wfState, nodeState);
case LLM_ANSWER -> wfNode = new LLMAnswerNode(wfComponent, nodeDefinition, wfState, nodeState);
case KEYWORD_EXTRACTOR -> wfNode = new KeywordExtractorNode(wfComponent, nodeDefinition, wfState, nodeState);
case TONGYI_WANX -> wfNode = new ImageNode(wfComponent, nodeDefinition, wfState, nodeState);
case KNOWLEDGE_RETRIEVER -> wfNode = new KnowledgeRetrievalNode(wfComponent, nodeDefinition, wfState, nodeState);
case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState);
case MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState);
case HTTP_REQUEST -> wfNode = new HttpRequestNode(wfComponent, nodeDefinition, wfState, nodeState);
case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState);
case HUMAN_FEEDBACK -> wfNode = new HumanFeedbackNode(wfComponent, nodeDefinition, wfState, nodeState);
case GOOGLE_SEARCH -> wfNode = new GoogleSearchNode(wfComponent, nodeDefinition, wfState, nodeState);
default -> {
}
}

View File

@@ -55,11 +55,6 @@ public class WfState {
private List<NodeIOData> output = new ArrayList<>();
private Integer processStatus = WORKFLOW_PROCESS_STATUS_READY;
/**
* 人机交互节点
*/
private Set<String> interruptNodes = new HashSet<>();
public WfState(User user, List<NodeIOData> input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter, Long sessionId) {
this.input = input;
this.user = user;
@@ -133,8 +128,4 @@ public class WfState {
.findFirst()
.orElse(null);
}
public void addInterruptNode(String nodeUuid) {
this.interruptNodes.add(nodeUuid);
}
}

View File

@@ -108,82 +108,39 @@ public class WorkflowEngine {
MemorySaver saver = new MemorySaver();
CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver)
.interruptBefore(wfState.getInterruptNodes().toArray(String[]::new))
.build();
app = mainStateGraph.compile(compileConfig);
RunnableConfig invokeConfig = RunnableConfig.builder().build();
exe(invokeConfig, false);
exe(invokeConfig);
} catch (Exception e) {
errorWhenExe(e);
}
}
private void exe(RunnableConfig invokeConfig, boolean resume) {
private void exe(RunnableConfig invokeConfig) {
//不使用langgraph4j state的update相关方法无需传入input
AsyncGenerator<NodeOutput<WfNodeState>> outputs = app.stream(resume ? null : Map.of(), invokeConfig);
AsyncGenerator<NodeOutput<WfNodeState>> outputs = app.stream(Map.of(), invokeConfig);
streamingResult(wfState, outputs, sseEmitter);
StateSnapshot<WfNodeState> stateSnapshot = app.getState(invokeConfig);
String nextNode = stateSnapshot.config().nextNode().orElse("");
//还有下个节点表示进入中断状态等待用户输入后继续执<E7BBAD>?
if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) {
// 获取提示模板
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.HUMAN_FEED_BACK.getValue());
// 获取人机交互提示信息
String intTip = nodeMessageTemplate + WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
//将等待输入信息[事件与提示词]发送到到客户端
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_WAIT_FEEDBACK_BY_" + nextNode + "]", intTip);
// 保存提示信息到Chat信息记录中对话使用
WorkflowMessageUtil.saveWorkflowMessage(wfState, intTip);
InterruptedFlow.RUNTIME_TO_GRAPH.put(wfState.getUuid(), this);
//更新状<E696B0>?
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_WAITING_INPUT);
workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
} else {
WorkflowRuntime updatedRuntime = workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
// 保存成功会话信息
wfNodes.stream().filter(item -> stateSnapshot.node().equals(item.getUuid()))
.findFirst().ifPresent(wfNode -> {
// 获取节点模板提示词信息
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.END.getValue());
// 发送SSE消息驱动事件和保存会话
WorkflowMessageUtil.notifyAndStoreMessage(wfState, sseEmitter, wfNode, nodeMessageTemplate);
});
// 发送结束消息
sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput());
// 发送驱动消息事件
InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid());
}
}
/**
* 中断流程等待用户输入时,会进行暂停状态,用户输入后调用本方法执行流程剩余部分
*
* @param userInput 用户输入
*/
public void resume(String userInput) {
RunnableConfig invokeConfig = RunnableConfig.builder().build();
try {
app.updateState(invokeConfig, Map.of(HUMAN_FEEDBACK_KEY, userInput), null);
exe(invokeConfig, true);
} catch (Exception e) {
errorWhenExe(e);
} finally {
//有可能多次接收人机交互,待整个流程完全执行后才能删除
if (wfState.getProcessStatus() != WORKFLOW_PROCESS_STATUS_WAITING_INPUT) {
InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid());
}
}
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_SUCCESS);
WorkflowRuntime updatedRuntime = workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
wfNodes.stream().filter(item -> stateSnapshot.node().equals(item.getUuid()))
.findFirst().ifPresent(wfNode -> {
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.END.getValue());
WorkflowMessageUtil.notifyAndStoreMessage(wfState, sseEmitter, wfNode, nodeMessageTemplate);
});
sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput());
}
private void errorWhenExe(Exception e) {
log.error("error", e);
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.EXCEPTION.getValue());
String errorMsg = e.getMessage();
if (errorMsg.contains("parallel node doesn't support conditional branch")) {
if (errorMsg != null && errorMsg.contains("parallel node doesn't support conditional branch")) {
errorMsg = "并行节点中不能包含条件分<EFBFBD>?";
}
errorMsg = nodeMessageTemplate + errorMsg;
errorMsg = nodeMessageTemplate + (errorMsg != null ? errorMsg : e.getClass().getSimpleName());
// 保存会话信息且发送驱动消息事件
WorkflowMessageUtil.saveWorkflowMessage(wfState, errorMsg);
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
@@ -267,6 +224,10 @@ public class WorkflowEngine {
log.info("node:{},chunk:{}", node, chunk);
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
} else {
// __END__ 是 langgraph4j 的终止伪节点, 无对应业务节点状态, 跳过
if (END.equals(out.node())) {
continue;
}
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
.filter(item -> item.getNode().getUuid().endsWith(out.node())).findFirst().orElse(null);
if (null != abstractWfNode) {

View File

@@ -19,7 +19,6 @@ import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async;
import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async;
import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
/**
* 负责构建工作流运行所依赖的状态图<E68081>?
@@ -27,7 +26,6 @@ import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
@Slf4j
public class WorkflowGraphBuilder {
private final Map<Long, WorkflowComponent> componentIndex;
private final Map<String, WorkflowNode> nodeIndex;
private final Map<String, List<WorkflowEdge>> edgesBySource;
private final Map<String, List<WorkflowEdge>> edgesByTarget;
@@ -46,8 +44,6 @@ public class WorkflowGraphBuilder {
List<WorkflowEdge> edges,
WorkflowNodeRunner nodeRunner,
WfState wfState) {
this.componentIndex = components.stream()
.collect(Collectors.toMap(WorkflowComponent::getId, Function.identity(), (origin, ignore) -> origin));
this.nodeIndex = nodes.stream()
.collect(Collectors.toMap(WorkflowNode::getUuid, Function.identity(), (origin, ignore) -> origin));
this.edgesBySource = edges.stream().collect(Collectors.groupingBy(WorkflowEdge::getSourceNodeUuid));
@@ -217,14 +213,6 @@ public class WorkflowGraphBuilder {
WorkflowNode wfNode = getNodeByUuid(stateGraphNodeUuid);
stateGraph.addNode(stateGraphNodeUuid, node_async(state -> nodeRunner.run(wfNode, state)));
stateGraphList.add(stateGraph);
WorkflowComponent component = componentIndex.get(wfNode.getWorkflowComponentId());
if (component == null) {
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
}
if (HUMAN_FEEDBACK.getName().equals(component.getName())) {
wfState.addInterruptNode(stateGraphNodeUuid);
}
}
private void addEdgeToStateGraph(StateGraph<WfNodeState> stateGraph, String source, String target) throws GraphStateException {

View File

@@ -6,9 +6,9 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.entity.User;
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
import org.ruoyi.common.core.exception.base.BaseException;
import org.ruoyi.common.satoken.utils.LoginHelper;
import org.ruoyi.common.sse.core.SseEmitterManager;
import org.ruoyi.common.tenant.helper.TenantHelper;
import org.ruoyi.workflow.entity.*;
import org.ruoyi.workflow.helper.SSEEmitterHelper;
import org.ruoyi.workflow.service.*;
@@ -58,6 +58,8 @@ public class WorkflowStarter implements IWorkFlowStarterService {
Long userId = LoginHelper.getUserId();
// 获取登录Token仅透传给 WfState工作流 SSE 通过 emitter 直发,不串台)
String tokenValue = StpUtil.getTokenValue();
// 获取当前租户ID@Async 线程不继承请求线程的租户上下文,需显式透传)
String tenantId = TenantHelper.getTenantId();
// 根据会话ID连接SSE对象每会话一个连接避免同用户多会话串台
SseEmitter sseEmitter = sseEmitterManager.connect(String.valueOf(sessionId));
if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) {
@@ -71,41 +73,33 @@ public class WorkflowStarter implements IWorkFlowStarterService {
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo());
return sseEmitter;
}
self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId);
self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId, tenantId);
return sseEmitter;
}
@Async
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId) {
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
.eq(WorkflowNode::getWorkflowId, workflow.getId())
.eq(WorkflowNode::getIsDeleted, false)
.list();
List<WorkflowEdge> edges = workflowEdgeService.lambdaQuery()
.eq(WorkflowEdge::getWorkflowId, workflow.getId())
.eq(WorkflowEdge::getIsDeleted, false)
.list();
WorkflowEngine workflowEngine = new WorkflowEngine(workflow,
sseEmitterHelper, components, nodes, edges,
workflowRuntimeService, workflowRuntimeNodeService);
workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue, sessionId);
}
@Async
public void resumeFlow(String runtimeUuid, String userInput, SseEmitter sseEmitter) {
WorkflowEngine workflowEngine = InterruptedFlow.RUNTIME_TO_GRAPH.get(runtimeUuid);
if (null == workflowEngine) {
log.error("工作流恢复执行时失败,runtime:{}", runtimeUuid);
throw new BaseException(A_WF_RESUME_FAIL.getInfo());
public void asyncRun(User user, Workflow workflow, List<ObjectNode> userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId, String tenantId) {
// @Async 线程不继承请求线程的租户上下文, 显式设置, 避免租户缓存/隔离逻辑异常
if (tenantId != null) {
TenantHelper.setDynamic(tenantId);
}
// 如果SSE连接对象不为空传入该对象Chat调用工作流对话使用
if (null != sseEmitter){
workflowEngine.setSseEmitter(sseEmitter);
// 为了让每个节点都可以发送模板消息 保持SSE对象一致以防出现向已关闭的SSE对象发送消息
workflowEngine.getWfState().setSseEmitter(sseEmitter);
try {
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
List<WorkflowComponent> components = workflowComponentService.getAllEnable();
List<WorkflowNode> nodes = workflowNodeService.lambdaQuery()
.eq(WorkflowNode::getWorkflowId, workflow.getId())
.eq(WorkflowNode::getIsDeleted, false)
.list();
List<WorkflowEdge> edges = workflowEdgeService.lambdaQuery()
.eq(WorkflowEdge::getWorkflowId, workflow.getId())
.eq(WorkflowEdge::getIsDeleted, false)
.list();
WorkflowEngine workflowEngine = new WorkflowEngine(workflow,
sseEmitterHelper, components, nodes, edges,
workflowRuntimeService, workflowRuntimeNodeService);
workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue, sessionId);
} finally {
TenantHelper.clearDynamic();
}
workflowEngine.resume(userInput);
}
}

View File

@@ -2,7 +2,6 @@ package org.ruoyi.workflow.workflow;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
@@ -20,7 +19,6 @@ import org.ruoyi.common.chat.factory.ImageServiceFactory;
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
import org.ruoyi.workflow.util.JsonUtil;
import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
@@ -88,22 +86,6 @@ public class WorkflowUtil{
return result;
}
public static String getHumanFeedbackTip(String nodeUuid, List<WorkflowNode> wfNodes) {
WorkflowNode wfNode = wfNodes.stream()
.filter(item -> item.getUuid().equals(nodeUuid))
.findFirst().orElse(null);
if (null == wfNode) {
return "";
}
String wfNodeNodeConfig = wfNode.getNodeConfig();
if (StrUtil.isBlank(wfNodeNodeConfig)) {
return "";
}
Map<String, Object> map = JsonUtil.toMap(wfNodeNodeConfig);
Object tip = map.getOrDefault("tip", "");
return String.valueOf(tip);
}
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
String prompt, String nodeMessageTemplate) {
log.info("stream invoke, modelName: {}", modelName);

View File

@@ -1,16 +0,0 @@
package org.ruoyi.workflow.workflow.node.classifier;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ClassifierNodeConfig {
private List categories = new ArrayList<>();
@JsonProperty("model_platform")
private String modelPlatform;
@JsonProperty("model_name")
private String modelName;
}

View File

@@ -3,23 +3,44 @@ package org.ruoyi.workflow.workflow.node.enmus;
import lombok.Getter;
/**
* 节点消息模板ConfigKey枚举
* 节点消息模板ConfigKey枚举 <br/>
* 模板优先从 sys_config 读取(可在系统管理-配置管理中自定义), 未配置时回退到 defaultTemplate 内置默认值
*/
@Getter
public enum NodeMessageTemplateEnum {
HTTP_REQUEST("node.httpRequest.template"),
MAIL_SEND("node.mailsend.template"),
IMAGE("node.image.template"),
HUMAN_FEED_BACK("node.humanFeedback.template"),
SWITCH("node.switch.template"),
LLM_RESPONSE("node.llmAnswer.template"),
KEYWORD_EXTRACTOR("node.keywordExtractor.template"),
EXCEPTION("node.exception.template"),
END("node.end.template");
HTTP_REQUEST("node.httpRequest.template", "✅ HTTP请求节点结束响应 - "),
MAIL_SEND("node.mailsend.template", "📧 发送邮箱节点:结束响应 - "),
IMAGE("node.image.template", "🎨 文生图节点:结束响应 - 图片URL: "),
SWITCH("node.switch.template", "🔀 条件分支节点:触发 -> 跳转到节点 "),
LLM_RESPONSE("node.llmAnswer.template", "🤖 LLM 节点 生成回答:"),
GOOGLE_SEARCH("node.googleSearch.template", "🔍 网络搜索节点处理完成:"),
EXCEPTION("node.exception.template", "🛑 工作流发生异常:"),
END("node.end.template", "🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。");
private final String value;
NodeMessageTemplateEnum(String value) {
/**
* 内置默认模板, sys_config 未配置对应键时使用
*/
private final String defaultTemplate;
NodeMessageTemplateEnum(String value, String defaultTemplate) {
this.value = value;
this.defaultTemplate = defaultTemplate;
}
/**
* 根据 configKey 获取内置默认模板, 未知键返回空串
*
* @param configKey sys_config 配置键
* @return 内置默认模板
*/
public static String getDefaultTemplate(String configKey) {
for (NodeMessageTemplateEnum item : values()) {
if (item.value.equals(configKey)) {
return item.defaultTemplate;
}
}
return "";
}
}

View File

@@ -0,0 +1,91 @@
package org.ruoyi.workflow.workflow.node.googleSearch;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.util.JsonUtil;
import org.ruoyi.workflow.util.SpringUtil;
import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.WorkflowUtil;
import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import java.util.List;
import java.util.UUID;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
/**
* 【扩展节点】网络搜索
* 通过智谱 Web Search API 返回适合大模型消费的结构化网页结果。
*/
@Slf4j
public class GoogleSearchNode extends AbstractWfNode {
public GoogleSearchNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
super(wfComponent, nodeDef, wfState, nodeState);
}
/**
* 处理搜索请求
* nodeConfig 格式:
* {
* "query": "搜索关键词",
* "search_engine": "search_std",
* "result_count": 10,
* "search_domain_filter": "",
* "search_recency_filter": "noLimit",
* "content_size": "medium",
* "include_image": false
* }
*
* @return 搜索结果
*/
@Override
public NodeProcessResult onProcess() {
GoogleSearchNodeConfig config = checkAndGetConfig(GoogleSearchNodeConfig.class);
// 获取搜索关键词
String searchQuery = WorkflowUtil.renderTemplate(config.getQuery(), state.getInputs());
if (StringUtils.isBlank(searchQuery)) {
searchQuery = getFirstInputText();
}
if (StringUtils.isBlank(searchQuery)) {
throw new IllegalArgumentException("未提供搜索关键词");
}
searchQuery = searchQuery.trim();
if (searchQuery.length() > 70) {
throw new IllegalArgumentException("搜索关键词不能超过 70 个字符");
}
log.info("Web search node processing, engine: {}, result_count: {}",
config.getSearchEngine(), config.getResultCount());
String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.GOOGLE_SEARCH.getValue());
notifyAndStoreMessage(wfState, nodeMessageTemplate);
ZhipuWebSearchClient searchClient = SpringUtil.getBean(ZhipuWebSearchClient.class);
ZhipuWebSearchClient.SearchResponse response = searchClient.search(
searchQuery,
config,
UUID.randomUUID().toString()
);
String searchResult = JsonUtil.toJson(response);
if (searchResult == null) {
throw new IllegalStateException("搜索结果序列化失败");
}
log.info("Web search completed, result count: {}", response.count());
notifyAndStoreMessage(wfState, nodeMessageTemplate + "返回 " + response.count() + " 条结果");
List<NodeIOData> outputs = List.of(
NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "智谱网络搜索结果", searchResult)
);
return NodeProcessResult.builder().content(outputs).build();
}
}

View File

@@ -0,0 +1,44 @@
package org.ruoyi.workflow.workflow.node.googleSearch;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Data
public class GoogleSearchNodeConfig {
/**
* 搜索查询关键词
*/
private String query;
@JsonProperty("search_engine")
@Pattern(
regexp = "search_std|search_pro|search_pro_sogou|search_pro_quark",
message = "搜索引擎参数无效"
)
private String searchEngine = "search_std";
@JsonProperty("result_count")
@Min(value = 1, message = "搜索结果数量不能小于 1")
@Max(value = 50, message = "搜索结果数量不能大于 50")
private Integer resultCount = 10;
@JsonProperty("search_domain_filter")
private String searchDomainFilter;
@JsonProperty("search_recency_filter")
@Pattern(
regexp = "oneDay|oneWeek|oneMonth|oneYear|noLimit",
message = "搜索时间范围参数无效"
)
private String searchRecencyFilter = "noLimit";
@JsonProperty("content_size")
@Pattern(regexp = "medium|high", message = "网页摘要长度参数无效")
private String contentSize = "medium";
@JsonProperty("include_image")
private Boolean includeImage = false;
}

View File

@@ -0,0 +1,155 @@
package org.ruoyi.workflow.workflow.node.googleSearch;
import ai.z.openapi.ZhipuAiClient;
import ai.z.openapi.service.web_search.WebSearchRequest;
import ai.z.openapi.service.web_search.WebSearchResp;
import ai.z.openapi.service.web_search.WebSearchResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 智谱 Web Search 官方 SDK 适配器。
*/
@Component
@RequiredArgsConstructor
public class ZhipuWebSearchClient {
private static final String ZHIPU_PROVIDER_CODE = "zhipu";
private static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas/v4/";
private final ZhipuWebSearchProperties properties;
private final IChatModelService chatModelService;
public SearchResponse search(String query, GoogleSearchNodeConfig config, String requestId) {
Credential credential = resolveCredential();
ZhipuAiClient client = createClient(credential);
try {
WebSearchRequest request = WebSearchRequest.builder()
.searchQuery(query)
.searchEngine(config.getSearchEngine())
.count(config.getResultCount())
.searchDomainFilter(blankToNull(config.getSearchDomainFilter()))
.searchRecencyFilter(config.getSearchRecencyFilter())
.contentSize(config.getContentSize())
.includeImage(config.getIncludeImage())
.requestId(requestId)
.build();
WebSearchResponse response = client.webSearch().createWebSearch(request);
if (response == null || !response.isSuccess() || response.getData() == null) {
String message = response == null ? "接口未返回响应" : StringUtils.defaultIfBlank(response.getMsg(), "未知错误");
throw new IllegalStateException("智谱 Web Search 调用失败:" + message);
}
List<SearchResult> results = response.getData().getWebSearchResp() == null
? List.of()
: response.getData().getWebSearchResp().stream()
.map(this::toSearchResult)
.toList();
return new SearchResponse(
query,
config.getSearchEngine(),
response.getData().getRequestId(),
results.size(),
results
);
} finally {
client.close();
}
}
private ZhipuAiClient createClient(Credential credential) {
int connectTimeout = positiveOrDefault(properties.getConnectTimeout(), 10);
int readTimeout = positiveOrDefault(properties.getReadTimeout(), 30);
return ZhipuAiClient.builder()
.ofZHIPU()
.apiKey(credential.apiKey())
.baseUrl(credential.baseUrl())
.networkConfig(connectTimeout, readTimeout, readTimeout, readTimeout, TimeUnit.SECONDS)
.enableTokenCache()
.build();
}
private Credential resolveCredential() {
if (isUsableApiKey(properties.getApiKey())) {
return new Credential(normalizeBaseUrl(properties.getBaseUrl()), properties.getApiKey().trim());
}
ChatModelBo query = new ChatModelBo();
query.setProviderCode(ZHIPU_PROVIDER_CODE);
return chatModelService.queryList(query).stream()
.filter(model -> isUsableApiKey(model.getApiKey()))
.findFirst()
.map(model -> new Credential(normalizeBaseUrl(model.getApiHost()), model.getApiKey().trim()))
.orElseThrow(() -> new IllegalStateException(
"未配置智谱 API Key请设置环境变量 ZAI_API_KEY或在模型管理中配置 zhipu 厂商密钥"
));
}
private boolean isUsableApiKey(String apiKey) {
return StringUtils.isNotBlank(apiKey)
&& !"sk_xx".equalsIgnoreCase(apiKey.trim())
&& !"your_api_key".equalsIgnoreCase(apiKey.trim());
}
private String normalizeBaseUrl(String baseUrl) {
String normalized = StringUtils.defaultIfBlank(baseUrl, DEFAULT_BASE_URL).trim();
normalized = StringUtils.removeEnd(normalized, "/");
if (!normalized.endsWith("/api/paas/v4")) {
normalized += "/api/paas/v4";
}
return normalized + "/";
}
private int positiveOrDefault(Integer value, int defaultValue) {
return value != null && value > 0 ? value : defaultValue;
}
private String blankToNull(String value) {
return StringUtils.isBlank(value) ? null : value.trim();
}
private SearchResult toSearchResult(WebSearchResp result) {
return new SearchResult(
result.getTitle(),
result.getContent(),
result.getLink(),
result.getMedia(),
result.getIcon(),
result.getRefer(),
result.getPublishDate(),
result.getImages()
);
}
private record Credential(String baseUrl, String apiKey) {
}
public record SearchResponse(
String query,
String searchEngine,
String requestId,
int count,
List<SearchResult> results
) {
}
public record SearchResult(
String title,
String content,
String link,
String media,
String icon,
String refer,
String publishDate,
List<String> images
) {
}
}

View File

@@ -0,0 +1,34 @@
package org.ruoyi.workflow.workflow.node.googleSearch;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 智谱 Web Search 配置。
*/
@Data
@Component
@ConfigurationProperties(prefix = "workflow.web-search.zhipu")
public class ZhipuWebSearchProperties {
/**
* 智谱国内开放平台 API 根地址。
*/
private String baseUrl = "https://open.bigmodel.cn/api/paas/v4/";
/**
* 智谱 API Key。建议通过环境变量 ZAI_API_KEY 注入。
*/
private String apiKey;
/**
* 连接超时秒数。
*/
private Integer connectTimeout = 10;
/**
* 读取超时秒数。
*/
private Integer readTimeout = 30;
}

View File

@@ -1,56 +0,0 @@
package org.ruoyi.workflow.workflow.node.humanFeedBack;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.WorkflowUtil;
import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*;
/**
* 人机交互节点实现类
*/
@Slf4j
public class HumanFeedbackNode extends AbstractWfNode {
public HumanFeedbackNode(WorkflowComponent component, WorkflowNode nodeDefinition, WfState wfState, WfNodeState nodeState) {
super(component, nodeDefinition, wfState, nodeState);
}
// 人机交互节点的处理逻辑
@Override
public NodeProcessResult onProcess() {
log.info("Processing HumanFeedback node: {}", node.getTitle());
// 从状态中获取用户输入数据
Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY);
if (null != humanFeedbackState) {
String userInput = humanFeedbackState.toString();
if (StringUtils.isNotBlank(userInput)) {
// 用户已提供输入,将用户输入添加到节点输入和输出中
NodeIOData feedbackData = NodeIOData.createByText("output", "default", userInput);
// 添加到输出列表,这样后续节点可以使用
state.getOutputs().add(feedbackData);
// 设置为成功状态
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
log.info("Human feedback processed for node: {}, content: {}", node.getTitle(), userInput);
} else {
// 用户输入为空,设置等待状态
state.setProcessStatus(NODE_PROCESS_STATUS_DOING);
log.info("Human feedback is empty for node: {}", node.getTitle());
}
} else {
// 没有用户输入,这可能是正常情况(等待用户输入)
// 但为了确保流程可以继续,我们仍然标记为成功
state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS);
log.info("No human feedback found for node: {}, continuing workflow", node.getTitle());
}
return new NodeProcessResult();
}
}

View File

@@ -1,106 +0,0 @@
package org.ruoyi.workflow.workflow.node.keywordExtractor;
import dev.langchain4j.data.message.SystemMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.util.SpringUtil;
import org.ruoyi.workflow.util.WorkflowMessageUtil;
import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.WorkflowUtil;
import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import java.util.ArrayList;
import java.util.List;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
/**
* 【节点】关键词提取节点
* 使用 LLM 从文本中提取关键词
*/
@Slf4j
public class KeywordExtractorNode extends AbstractWfNode {
public KeywordExtractorNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
super(wfComponent, nodeDef, wfState, nodeState);
}
/**
* 处理关键词提取
* nodeConfig 格式:
* {
* "model_name": "deepseek-chat",
* "category": "llm",
* "top_n": 5,
* "prompt": "额外的提示词"
* }
*
* @return 提取的关键词列表
*/
@Override
public NodeProcessResult onProcess() {
KeywordExtractorNodeConfig config = checkAndGetConfig(KeywordExtractorNodeConfig.class);
// 获取输入文本
String inputText = getFirstInputText();
if (StringUtils.isBlank(inputText)) {
log.warn("Keyword extractor node has no input text, node: {}", state.getUuid());
// 返回空结果
List<NodeIOData> outputs = new ArrayList<>();
outputs.add(NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", ""));
return NodeProcessResult.builder().content(outputs).build();
}
log.info("Keyword extractor node config: {}", config);
log.info("Input text length: {}", inputText.length());
// 构建提示词
String prompt = buildPrompt(config, inputText);
log.info("Keyword extraction prompt: {}", prompt);
// 调用 LLM 进行关键词提取
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = config.getModelName();
// 获取节点模板提示词信息
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
// 发送SSE事件消息
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
// 使用流式调用
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
return new NodeProcessResult();
}
/**
* 构建关键词提取的提示词
*/
private String buildPrompt(KeywordExtractorNodeConfig config, String inputText) {
StringBuilder promptBuilder = new StringBuilder();
// 基础提示词
promptBuilder.append("请从以下文本中提取 ").append(config.getTopN()).append(" 个最重要的关键词。\n\n");
// 添加自定义提示词(如果有)
if (StringUtils.isNotBlank(config.getPrompt())) {
promptBuilder.append(config.getPrompt()).append("\n\n");
}
// 输出格式要求
promptBuilder.append("要求:\n");
promptBuilder.append("1. 只返回关键词,每个关键词用逗号分隔\n");
promptBuilder.append("2. 关键词应该是名词或名词短语\n");
promptBuilder.append("3. 按重要性从高到低排序\n");
promptBuilder.append("4. 不要添加任何解释或额外的文字\n\n");
// 原始文本
promptBuilder.append("文本内容:\n");
promptBuilder.append(inputText);
return promptBuilder.toString();
}
}

View File

@@ -1,42 +0,0 @@
package org.ruoyi.workflow.workflow.node.keywordExtractor;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 关键词提取节点配置
*/
@EqualsAndHashCode
@Data
public class KeywordExtractorNodeConfig {
/**
* 模型分类llm, embedding 等)
*/
private String category;
/**
* 模型名称
*/
@NotNull
@JsonProperty("model_name")
private String modelName;
/**
* 提取的关键词数量
*/
@Min(1)
@Max(50)
@JsonProperty("top_n")
private Integer topN = 5;
/**
* 提示词(可选)
* 用于指导关键词提取的额外说明
*/
private String prompt;
}

View File

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.util.JsonUtil;
import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
@@ -40,15 +41,25 @@ public class MailSendNode extends AbstractWfNode {
String input = getDataFromInput(inputs);
// 判断是否为JSON格式(LLM输出转换 由LLM生成格式)
if (StringUtils.isNotBlank(input) && isJson(input)) {
// 使用Jackson解析和合并配置
ObjectMapper objectMapper = new ObjectMapper();
JsonNode inputJson = objectMapper.readTree(input);
// 将config转换为JsonNode
JsonNode configJson = objectMapper.valueToTree(config);
// 合并两个JSON节点
JsonNode mergedJson = objectMapper.readerForUpdating(configJson).readValue(inputJson);
// 转换回config对象
config = objectMapper.treeToValue(mergedJson, MailSendNodeConfig.class);
try {
// 使用统一的 JsonUtil 进行解析
JsonNode inputJson = JsonUtil.toJsonNode(input);
if (inputJson != null) {
// 使用 JsonUtil 内部的 ObjectMapper 进行合并
ObjectMapper objectMapper = new ObjectMapper();
// 将config转换为JsonNode
JsonNode configJson = objectMapper.valueToTree(config);
// 合并两个JSON节点
JsonNode mergedJson = objectMapper.readerForUpdating(configJson).readValue(inputJson);
// 转换回config对象
config = objectMapper.treeToValue(mergedJson, MailSendNodeConfig.class);
} else {
log.warn("输入 JSON 解析结果为 null使用原始配置");
}
} catch (Exception e) {
log.error("合并邮件配置失败,使用原始配置: {}", e.getMessage(), e);
// 继续使用原始 config不中断流程
}
}
// 安全获取模板(使用 defaultString 避免 null

View File

@@ -1,7 +1,6 @@
package org.ruoyi.workflow.workflow.node.switcher;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -9,6 +8,7 @@ import org.ruoyi.common.core.utils.SpringUtils;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.service.WorkflowNodeService;
import org.ruoyi.workflow.util.JsonUtil;
import org.ruoyi.workflow.workflow.NodeProcessResult;
import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
@@ -18,8 +18,6 @@ import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* 条件分支节点
@@ -339,9 +337,12 @@ public class SwitcherNode extends AbstractWfNode {
log.info("节点 '{}' 的输入配置: {}", nodeUuid, inputConfig);
if (StringUtils.isNotBlank(inputConfig)){
try {
// 使用Jackson解析输入配置
ObjectMapper objectMapper = new ObjectMapper();
JsonNode configJson = objectMapper.readTree(inputConfig);
// 使用统一的 JsonUtil 而不是每次创建新的 ObjectMapper
JsonNode configJson = JsonUtil.toJsonNode(inputConfig);
if (configJson == null) {
log.warn("节点 '{}' 的输入配置 JSON 解析结果为 null", nodeUuid);
return result;
}
// 获取 user_inputs 数组
JsonNode userInputs = configJson.get("user_inputs");
if (userInputs != null && userInputs.isArray()) {
@@ -357,7 +358,8 @@ public class SwitcherNode extends AbstractWfNode {
}
}
} catch (Exception e) {
log.error("解析节点输入配置失败: {}", nodeUuid, e);
log.error("解析节点 '{}' 输入配置失败,参数名: {}, 配置内容: {}", nodeUuid, paramName, inputConfig, e);
// 不抛出异常,返回默认结果,避免中断整个流程
}
}
}

View File

@@ -0,0 +1,21 @@
-- =============================================
-- 流程编排搜索节点配置脚本
-- =============================================
-- 说明:本脚本用于添加搜索节点的系统配置
-- 执行前请确保 sys_config 表存在
-- =============================================
-- 搜索节点模板配置
INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time, update_by, update_time)
VALUES ('搜索节点模板', 'node.googleSearch.template', '正在搜索相关内容...', 'Y', '搜索节点的响应模板,用于网络搜索功能', 'admin', NOW(), 'admin', NOW())
ON DUPLICATE KEY UPDATE config_value = '正在搜索相关内容...', update_time = NOW();
-- =============================================
-- 验证配置是否添加成功
-- =============================================
SELECT config_id, config_name, config_key, config_value, config_type, remark
FROM sys_config
WHERE config_key IN (
'node.googleSearch.template'
)
ORDER BY config_id;