初始化
This commit is contained in:
67
easy-agents-flow/pom.xml
Normal file
67
easy-agents-flow/pom.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easy-agents-flow</name>
|
||||
<artifactId>easy-agents-flow</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.29</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.9.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>2.0.58</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jfinal</groupId>
|
||||
<artifactId>enjoy</artifactId>
|
||||
<version>5.1.3</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.graalvm.js</groupId>
|
||||
<artifactId>js-scriptengine</artifactId>
|
||||
<version>21.3.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.graalvm.js</groupId>
|
||||
<artifactId>js</artifactId>
|
||||
<version>21.3.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.ibm.icu</groupId>
|
||||
<artifactId>icu4j</artifactId>
|
||||
<version>77.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,748 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.easyagents.flow.core.chain.event.*;
|
||||
import com.easyagents.flow.core.chain.repository.*;
|
||||
import com.easyagents.flow.core.chain.runtime.*;
|
||||
import com.easyagents.flow.core.util.CollectionUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
||||
public class Chain {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Chain.class);
|
||||
private static final ThreadLocal<Chain> EXECUTION_THREAD_LOCAL = new ThreadLocal<>();
|
||||
|
||||
|
||||
protected final ChainDefinition definition;
|
||||
protected String stateInstanceId;
|
||||
|
||||
// protected final ChainState state;
|
||||
protected ChainStateRepository chainStateRepository;
|
||||
protected NodeStateRepository nodeStateRepository;
|
||||
protected EventManager eventManager;
|
||||
protected TriggerScheduler triggerScheduler;
|
||||
|
||||
public static Chain currentChain() {
|
||||
return EXECUTION_THREAD_LOCAL.get();
|
||||
}
|
||||
|
||||
public Chain(ChainDefinition definition, String stateInstanceId) {
|
||||
this.definition = definition;
|
||||
this.stateInstanceId = stateInstanceId;
|
||||
}
|
||||
|
||||
public void notifyEvent(Event event) {
|
||||
eventManager.notifyEvent(event, this);
|
||||
}
|
||||
|
||||
public void setStatusAndNotifyEvent(ChainStatus status) {
|
||||
AtomicReference<ChainStatus> before = new AtomicReference<>();
|
||||
updateStateSafely(state -> {
|
||||
before.set(state.getStatus());
|
||||
state.setStatus(status);
|
||||
return EnumSet.of(ChainStateField.STATUS);
|
||||
});
|
||||
notifyEvent(new ChainStatusChangeEvent(this, status, before.get()));
|
||||
}
|
||||
|
||||
public void setStatusAndNotifyEvent(String stateInstanceId, ChainStatus status) {
|
||||
AtomicReference<ChainStatus> before = new AtomicReference<>();
|
||||
updateStateSafely(stateInstanceId, state -> {
|
||||
before.set(state.getStatus());
|
||||
state.setStatus(status);
|
||||
return EnumSet.of(ChainStateField.STATUS);
|
||||
});
|
||||
notifyEvent(new ChainStatusChangeEvent(this, status, before.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely updates the chain state with optimistic locking and retry-on-conflict.
|
||||
*
|
||||
* @param modifier the modifier that applies changes and declares updated fields
|
||||
* @throws ChainUpdateTimeoutException if update cannot succeed within timeout
|
||||
*/
|
||||
public ChainState updateStateSafely(ChainStateModifier modifier) {
|
||||
return updateStateSafely(this.stateInstanceId, modifier);
|
||||
}
|
||||
|
||||
|
||||
public ChainState updateStateSafely(String stateInstanceId, ChainStateModifier modifier) {
|
||||
final long timeoutMs = 30_000; // 30 seconds total timeout
|
||||
final long maxRetryDelayMs = 100; // Maximum delay between retries
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
int attempt = 0;
|
||||
ChainState current = null;
|
||||
while (System.currentTimeMillis() - startTime < timeoutMs) {
|
||||
current = chainStateRepository.load(stateInstanceId);
|
||||
if (current == null) {
|
||||
throw new IllegalStateException("Chain state not found: " + stateInstanceId);
|
||||
}
|
||||
|
||||
EnumSet<ChainStateField> updatedFields = modifier.modify(current);
|
||||
if (updatedFields == null || updatedFields.isEmpty()) {
|
||||
return current; // No actual changes, exit early
|
||||
}
|
||||
|
||||
if (chainStateRepository.tryUpdate(current, updatedFields)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
// Prepare next retry
|
||||
attempt++;
|
||||
long nextDelay = calculateNextRetryDelay(attempt, maxRetryDelayMs);
|
||||
sleepUninterruptibly(nextDelay);
|
||||
}
|
||||
|
||||
// Timeout reached
|
||||
assert current != null;
|
||||
String msg = String.format(
|
||||
"Chain state update timeout after %d ms (instanceId: %s)",
|
||||
timeoutMs, current.getInstanceId()
|
||||
);
|
||||
log.warn(msg);
|
||||
throw new ChainUpdateTimeoutException(msg);
|
||||
}
|
||||
|
||||
|
||||
public NodeState updateNodeStateSafely(String nodeId, NodeStateModifier modifier) {
|
||||
return this.updateNodeStateSafely(this.stateInstanceId, nodeId, modifier);
|
||||
}
|
||||
|
||||
public NodeState updateNodeStateSafely(String stateInstanceId, String nodeId, NodeStateModifier modifier) {
|
||||
final long timeoutMs = 30_000;
|
||||
final long maxRetryDelayMs = 100;
|
||||
long startTime = System.currentTimeMillis();
|
||||
int attempt = 0;
|
||||
|
||||
while (System.currentTimeMillis() - startTime < timeoutMs) {
|
||||
// 1. 加载最新 ChainState(获取 chainVersion)
|
||||
ChainState chainState = chainStateRepository.load(stateInstanceId);
|
||||
if (chainState == null) {
|
||||
throw new IllegalStateException("Chain state not found");
|
||||
}
|
||||
|
||||
// 2. 加载 NodeState
|
||||
NodeState nodeState = nodeStateRepository.load(stateInstanceId, nodeId);
|
||||
if (nodeState == null) {
|
||||
nodeState = new NodeState();
|
||||
nodeState.setChainInstanceId(chainState.getInstanceId());
|
||||
nodeState.setNodeId(nodeId);
|
||||
}
|
||||
|
||||
// 3. 应用修改
|
||||
EnumSet<NodeStateField> updatedFields = modifier.modify(nodeState);
|
||||
|
||||
if (updatedFields == null || updatedFields.isEmpty()) {
|
||||
return nodeState;
|
||||
}
|
||||
|
||||
// 4. 尝试更新(传入 chainVersion 保证一致性)
|
||||
if (nodeStateRepository.tryUpdate(nodeState, updatedFields, chainState.getVersion())) {
|
||||
return nodeState;
|
||||
}
|
||||
|
||||
// 5. 退避重试
|
||||
attempt++;
|
||||
sleepUninterruptibly(calculateNextRetryDelay(attempt, maxRetryDelayMs));
|
||||
}
|
||||
|
||||
throw new ChainUpdateTimeoutException("Node state update timeout");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the next retry delay using exponential backoff with jitter.
|
||||
*
|
||||
* @param attempt the current retry attempt (1-based)
|
||||
* @param maxDelayMs the maximum delay in milliseconds
|
||||
* @return the delay in milliseconds to wait before next retry
|
||||
*/
|
||||
private long calculateNextRetryDelay(int attempt, long maxDelayMs) {
|
||||
// Base delay: 10ms * (2^(attempt-1))
|
||||
long baseDelay = 10L * (1L << (attempt - 1));
|
||||
|
||||
// Add jitter: ±25% randomness to avoid thundering herd
|
||||
double jitterFactor = 0.75 + (Math.random() * 0.5); // [0.75, 1.25)
|
||||
long delayWithJitter = (long) (baseDelay * jitterFactor);
|
||||
|
||||
// Clamp between 1ms and maxDelayMs
|
||||
return Math.max(1L, Math.min(delayWithJitter, maxDelayMs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleeps for the specified duration, silently ignoring interrupts
|
||||
* but preserving the interrupt status.
|
||||
*
|
||||
* @param millis the length of time to sleep in milliseconds
|
||||
*/
|
||||
private void sleepUninterruptibly(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // Preserve interrupt status
|
||||
// Do NOT throw here — we want to continue retrying
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void start(Map<String, Object> variables) {
|
||||
Trigger prev = TriggerContext.getCurrentTrigger();
|
||||
try {
|
||||
// start 可能在 node 里执行一个新的 chain 的情况,
|
||||
// 需要清空父级 chain 的 Trigger
|
||||
TriggerContext.setCurrentTrigger(null);
|
||||
updateStateSafely(state -> {
|
||||
EnumSet<ChainStateField> fields = EnumSet.of(ChainStateField.STATUS);
|
||||
state.setStatus(ChainStatus.RUNNING);
|
||||
|
||||
if (variables != null && !variables.isEmpty()) {
|
||||
state.getMemory().putAll(variables);
|
||||
fields.add(ChainStateField.MEMORY);
|
||||
}
|
||||
|
||||
if (StringUtil.noText(state.getChainDefinitionId())) {
|
||||
state.setChainDefinitionId(definition.getId());
|
||||
fields.add(ChainStateField.CHAIN_DEFINITION_ID);
|
||||
}
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
notifyEvent(new ChainStartEvent(this, variables));
|
||||
setStatusAndNotifyEvent(ChainStatus.RUNNING);
|
||||
|
||||
// 调度入口节点
|
||||
List<Node> startNodes = definition.getStartNodes();
|
||||
for (Node startNode : startNodes) {
|
||||
scheduleNode(startNode, null, TriggerType.START, 0);
|
||||
}
|
||||
} finally {
|
||||
// 恢复父级 chain 的 Trigger
|
||||
TriggerContext.setCurrentTrigger(prev);
|
||||
}
|
||||
}
|
||||
|
||||
public void executeNode(Node node, Trigger trigger) {
|
||||
try {
|
||||
EXECUTION_THREAD_LOCAL.set(this);
|
||||
ChainState chainState = getState();
|
||||
|
||||
// 当前处于挂起状态
|
||||
if (chainState.getStatus() == ChainStatus.SUSPEND) {
|
||||
updateStateSafely(state -> {
|
||||
chainState.addSuspendNodeId(node.getId());
|
||||
return EnumSet.of(ChainStateField.SUSPEND_NODE_IDS);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 处于非运行状态,比如错误状态
|
||||
else if (chainState.getStatus() != ChainStatus.RUNNING) {
|
||||
return;
|
||||
}
|
||||
|
||||
String triggerEdgeId = trigger.getEdgeId();
|
||||
if (shouldSkipNode(node, triggerEdgeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> nodeResult = null;
|
||||
Throwable error = null;
|
||||
try {
|
||||
NodeState nodeState = getNodeState(node.id);
|
||||
|
||||
// 如果节点状态不是运行中,则更新为运行中
|
||||
// 目前只有 Loop 节点会处于 Running 状态,因为它会多次触发
|
||||
if (nodeState.getStatus() != NodeStatus.RUNNING) {
|
||||
updateNodeStateSafely(node.id, s -> {
|
||||
s.setStatus(NodeStatus.RUNNING);
|
||||
s.recordExecute(triggerEdgeId);
|
||||
return EnumSet.of(NodeStateField.EXECUTE_COUNT, NodeStateField.EXECUTE_EDGE_IDS, NodeStateField.STATUS);
|
||||
});
|
||||
TriggerType type = trigger.getType();
|
||||
notifyEvent(new NodeStartEvent(this, node));
|
||||
}
|
||||
// 只需记录执行次数
|
||||
else {
|
||||
updateNodeStateSafely(node.id, s -> {
|
||||
s.recordExecute(triggerEdgeId);
|
||||
return EnumSet.of(NodeStateField.EXECUTE_COUNT, NodeStateField.EXECUTE_EDGE_IDS);
|
||||
});
|
||||
}
|
||||
|
||||
updateStateSafely(state -> {
|
||||
state.addTriggerNodeId(node.id);
|
||||
return EnumSet.of(ChainStateField.TRIGGER_NODE_IDS);
|
||||
});
|
||||
|
||||
nodeResult = node.execute(this);
|
||||
} catch (Throwable throwable) {
|
||||
log.error("Node execute error", throwable);
|
||||
error = throwable;
|
||||
}
|
||||
handleNodeResult(node, nodeResult, triggerEdgeId, error);
|
||||
} finally {
|
||||
EXECUTION_THREAD_LOCAL.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public NodeState getNodeState(String nodeId) {
|
||||
return getNodeState(this.stateInstanceId, nodeId);
|
||||
}
|
||||
|
||||
public NodeState getNodeState(String stateInstanceId, String nodeId) {
|
||||
return nodeStateRepository.load(stateInstanceId, nodeId);
|
||||
}
|
||||
|
||||
public <T> T executeWithLock(String instanceId, long timeout, TimeUnit unit, Supplier<T> action) {
|
||||
try (ChainLock lock = chainStateRepository.getLock(instanceId, timeout, unit)) {
|
||||
if (!lock.isAcquired()) {
|
||||
throw new ChainLockTimeoutException("Failed to acquire lock for instance: " + instanceId);
|
||||
}
|
||||
return action.get();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldSkipNode(Node node, String edgeId) {
|
||||
return executeWithLock(stateInstanceId, 10, TimeUnit.SECONDS, () -> {
|
||||
NodeState newState = updateNodeStateSafely(node.id, s -> {
|
||||
s.recordTrigger(edgeId);
|
||||
return EnumSet.of(NodeStateField.TRIGGER_COUNT, NodeStateField.TRIGGER_EDGE_IDS);
|
||||
});
|
||||
|
||||
NodeCondition condition = node.getCondition();
|
||||
if (condition == null) {
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> prevResult = Collections.emptyMap();
|
||||
boolean shouldSkipNode = !condition.check(this, newState, prevResult);
|
||||
if (shouldSkipNode) {
|
||||
updateStateSafely(state -> {
|
||||
state.addUncheckedNodeId(node.id);
|
||||
return EnumSet.of(ChainStateField.UNCHECKED_NODE_IDS);
|
||||
});
|
||||
} else {
|
||||
updateStateSafely(state -> {
|
||||
if (state.removeUncheckedNodeId(node.id)) {
|
||||
return EnumSet.of(ChainStateField.UNCHECKED_NODE_IDS);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
return shouldSkipNode;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void handleNodeResult(Node node, Map<String, Object> prevNodeResult, String triggerEdgeId, Throwable error) {
|
||||
ChainStatus finalChainStatus = null;
|
||||
NodeStatus finalNodeStatus = null;
|
||||
try {
|
||||
if (error == null) {
|
||||
// 更新 state 数据
|
||||
updateStateSafely(state -> {
|
||||
EnumSet<ChainStateField> fields = EnumSet.of(ChainStateField.EXECUTE_RESULT);
|
||||
state.setExecuteResult(prevNodeResult);
|
||||
|
||||
if (prevNodeResult != null && !prevNodeResult.isEmpty()) {
|
||||
prevNodeResult.forEach((k, v) -> {
|
||||
if (v != null) {
|
||||
state.getMemory().put(node.getId() + "." + k, v);
|
||||
}
|
||||
});
|
||||
fields.add(ChainStateField.MEMORY);
|
||||
}
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
if (node.isRetryEnable() && node.isResetRetryCountAfterNormal()) {
|
||||
updateNodeStateSafely(node.id, state -> {
|
||||
state.setRetryCount(0);
|
||||
return EnumSet.of(NodeStateField.RETRY_COUNT);
|
||||
});
|
||||
}
|
||||
|
||||
finalNodeStatus = prevNodeResult == null ? null : (NodeStatus) prevNodeResult.get(ChainConsts.NODE_STATE_STATUS_KEY);
|
||||
|
||||
// 不调度下一个节点,由 node 自行调度,比如 Loop 循环
|
||||
Boolean scheduleNextNodeDisabled = prevNodeResult == null ? null : (Boolean) prevNodeResult.get(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY);
|
||||
if (scheduleNextNodeDisabled != null && scheduleNextNodeDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 结束节点
|
||||
finalChainStatus = prevNodeResult != null ? (ChainStatus) prevNodeResult.get(ChainConsts.CHAIN_STATE_STATUS_KEY) : null;
|
||||
if (finalChainStatus != null && finalChainStatus.isTerminal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 调度下一个节点
|
||||
scheduleNextForNode(node, prevNodeResult, triggerEdgeId);
|
||||
} else {
|
||||
// 挂起
|
||||
if (error instanceof ChainSuspendException) {
|
||||
updateNodeStateSafely(node.id, s -> {
|
||||
s.setStatus(NodeStatus.SUSPEND);
|
||||
return EnumSet.of(NodeStateField.STATUS);
|
||||
});
|
||||
|
||||
updateStateSafely(s -> {
|
||||
s.addSuspendNodeId(node.getId());
|
||||
s.addSuspendForParameters(((ChainSuspendException) error).getSuspendParameters());
|
||||
return EnumSet.of(ChainStateField.SUSPEND_NODE_IDS, ChainStateField.SUSPEND_FOR_PARAMETERS);
|
||||
});
|
||||
|
||||
finalNodeStatus = NodeStatus.SUSPEND;
|
||||
finalChainStatus = ChainStatus.SUSPEND;
|
||||
}
|
||||
// 失败
|
||||
else {
|
||||
NodeState newState = updateNodeStateSafely(node.getId(), s -> {
|
||||
s.setStatus(NodeStatus.ERROR);
|
||||
s.setError(new ExceptionSummary(error));
|
||||
return EnumSet.of(NodeStateField.ERROR, NodeStateField.STATUS);
|
||||
});
|
||||
|
||||
eventManager.notifyNodeError(error, node, prevNodeResult, this);
|
||||
|
||||
if (node.isRetryEnable()
|
||||
&& node.getMaxRetryCount() > 0
|
||||
&& newState.getRetryCount() < node.getMaxRetryCount()) {
|
||||
|
||||
updateNodeStateSafely(node.getId(), s -> {
|
||||
s.setRetryCount(s.getRetryCount() + 1);
|
||||
return EnumSet.of(NodeStateField.RETRY_COUNT);
|
||||
});
|
||||
|
||||
scheduleNode(node, triggerEdgeId, TriggerType.RETRY, node.getRetryIntervalMs());
|
||||
} else {
|
||||
finalChainStatus = handleNodeError(node.id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// 如果当前的工作流正在执行中,则不发送 NodeEndEvent 事件
|
||||
if (finalNodeStatus != NodeStatus.RUNNING) {
|
||||
NodeStatus nodeStatus = finalNodeStatus == null ? NodeStatus.SUCCEEDED : finalNodeStatus;
|
||||
updateNodeStateSafely(node.id, state -> {
|
||||
state.setStatus(nodeStatus);
|
||||
return EnumSet.of(NodeStateField.STATUS);
|
||||
});
|
||||
notifyEvent(new NodeEndEvent(this, node, prevNodeResult, error));
|
||||
}
|
||||
|
||||
if (finalChainStatus != null) {
|
||||
setStatusAndNotifyEvent(finalChainStatus);
|
||||
|
||||
// chain 执行结束
|
||||
if (finalChainStatus.isTerminal()) {
|
||||
eventManager.notifyEvent(new ChainEndEvent(this), this);
|
||||
|
||||
// 执行结束,但是未执行成功,失败和取消等
|
||||
// 更新父级链的状态
|
||||
if (!finalChainStatus.isSuccess()) {
|
||||
ChainState currentState = getState();
|
||||
ChainStatus currentStatus = finalChainStatus;
|
||||
while (currentState != null && StringUtil.hasText(currentState.getParentInstanceId())) {
|
||||
updateStateSafely(currentState.getParentInstanceId(), state -> {
|
||||
state.setStatus(currentStatus);
|
||||
return EnumSet.of(ChainStateField.STATUS);
|
||||
});
|
||||
setStatusAndNotifyEvent(currentState.getParentInstanceId(), currentStatus);
|
||||
currentState = getState(currentState.getParentInstanceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定节点调度下一次执行
|
||||
*
|
||||
* @param node 要调度的节点
|
||||
* @param result 节点执行结果
|
||||
* @param byEdigeId 触发边的ID
|
||||
*/
|
||||
private void scheduleNextForNode(Node node, Map<String, Object> result, String byEdigeId) {
|
||||
// 如果节点不支持循环,则直接调度向外的节点
|
||||
if (!node.isLoopEnable()) {
|
||||
scheduleOutwardNodes(node, result);
|
||||
return;
|
||||
}
|
||||
|
||||
NodeState nodeState = getNodeState(node.getId());
|
||||
// 如果达到最大循环次数限制,则调度向外的节点
|
||||
if (node.getMaxLoopCount() > 0 && nodeState.getLoopCount() >= node.getMaxLoopCount()) {
|
||||
scheduleOutwardNodes(node, result);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查循环中断条件,如果满足则调度向外的节点
|
||||
NodeCondition breakCondition = node.getLoopBreakCondition();
|
||||
if (breakCondition != null && breakCondition.check(this, nodeState, result)) {
|
||||
scheduleOutwardNodes(node, result);
|
||||
return;
|
||||
}
|
||||
|
||||
// 增加循环计数并重新调度当前节点
|
||||
updateNodeStateSafely(node.getId(), s -> {
|
||||
s.setLoopCount(s.getLoopCount() + 1);
|
||||
return EnumSet.of(NodeStateField.LOOP_COUNT);
|
||||
});
|
||||
|
||||
scheduleNode(node, byEdigeId, TriggerType.LOOP, node.getLoopIntervalMs());
|
||||
}
|
||||
|
||||
|
||||
private void scheduleOutwardNodes(Node node, Map<String, Object> result) {
|
||||
List<Edge> edges = definition.getOutwardEdge(node.getId());
|
||||
if (!CollectionUtil.hasItems(edges)) {
|
||||
// 当前节点没有向外的边,则调度父节点(自动回归父节点) 用在 Loop 循环等场景
|
||||
if (StringUtil.hasText(node.getParentId())) {
|
||||
Node parent = definition.getNodeById(node.getParentId());
|
||||
scheduleNode(parent, null, TriggerType.PARENT, 0L);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有向外的边是不是同一个父节点
|
||||
boolean allNotSameParent = false;
|
||||
boolean scheduleSuccess = false;
|
||||
for (Edge edge : edges) {
|
||||
Node nextNode = definition.getNodeById(edge.getTarget());
|
||||
if (nextNode == null) {
|
||||
throw new ChainException("Invalid edge target: " + edge.getTarget());
|
||||
}
|
||||
|
||||
// 如果存在不同父节点的边,则跳过, 比如 Loop 节点可能只有子节点,没有后续的节点
|
||||
if (!isSameParent(node, nextNode)) {
|
||||
allNotSameParent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
allNotSameParent = false;
|
||||
EdgeCondition edgeCondition = edge.getCondition();
|
||||
if (edgeCondition == null) {
|
||||
scheduleNode(nextNode, edge.getId(), TriggerType.NEXT, 0L);
|
||||
scheduleSuccess = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (edgeCondition.check(this, edge, result)) {
|
||||
updateStateSafely(state -> {
|
||||
if (state.removeUncheckedEdgeId(edge.getId())) {
|
||||
return EnumSet.of(ChainStateField.UNCHECKED_EDGE_IDS);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scheduleNode(nextNode, edge.getId(), TriggerType.NEXT, 0L);
|
||||
scheduleSuccess = true;
|
||||
} else {
|
||||
updateStateSafely(state -> {
|
||||
state.addUncheckedEdgeId(edge.getId());
|
||||
return EnumSet.of(ChainStateField.UNCHECKED_EDGE_IDS);
|
||||
});
|
||||
eventManager.notifyEvent(new EdgeConditionCheckFailedEvent(this, edge, node, result), this);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有向外的边都不满足条件,则调度父节点(自动回归父节点) 用在 Loop 循环嵌套等场景(Loop 下的第一个节点是 Loop)
|
||||
if (allNotSameParent && !scheduleSuccess) {
|
||||
if (StringUtil.hasText(node.getParentId())) {
|
||||
Node parent = definition.getNodeById(node.getParentId());
|
||||
scheduleNode(parent, null, TriggerType.PARENT, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个节点是否具有相同的父节点
|
||||
*
|
||||
* @param node 第一个节点
|
||||
* @param next 第二个节点
|
||||
* @return 如果两个节点的父节点ID相同则返回true,否则返回false
|
||||
*/
|
||||
private boolean isSameParent(Node node, Node next) {
|
||||
// 如果两个节点的父节点ID都为空或空白,则认为是相同父节点
|
||||
if (StringUtil.noText(node.getParentId()) && StringUtil.noText(next.getParentId())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 比较两个节点的父节点ID是否相等
|
||||
return node.getParentId() != null && node.getParentId().equals(next.getParentId());
|
||||
}
|
||||
|
||||
|
||||
public void scheduleNode(Node node, String edgeId, TriggerType type, long delayMs) {
|
||||
Trigger trigger = new Trigger();
|
||||
trigger.setStateInstanceId(stateInstanceId);
|
||||
trigger.setEdgeId(edgeId);
|
||||
trigger.setNodeId(node.getId());
|
||||
trigger.setType(type);
|
||||
trigger.setTriggerAt(System.currentTimeMillis() + delayMs);
|
||||
|
||||
if (edgeId != null) {
|
||||
updateStateSafely(state -> {
|
||||
state.addTriggerEdgeId(edgeId);
|
||||
return EnumSet.of(ChainStateField.TRIGGER_EDGE_IDS);
|
||||
});
|
||||
|
||||
eventManager.notifyEvent(new EdgeTriggerEvent(this, trigger), this);
|
||||
}
|
||||
|
||||
getTriggerScheduler().schedule(trigger);
|
||||
}
|
||||
|
||||
|
||||
private ChainStatus handleNodeError(String nodeId, Throwable throwable) {
|
||||
updateNodeStateSafely(nodeId, s -> {
|
||||
s.setStatus(NodeStatus.FAILED);
|
||||
s.setError(new ExceptionSummary(throwable));
|
||||
return EnumSet.of(NodeStateField.ERROR, NodeStateField.STATUS);
|
||||
});
|
||||
|
||||
updateStateSafely(state -> {
|
||||
state.setError(new ExceptionSummary(throwable));
|
||||
return EnumSet.of(ChainStateField.ERROR);
|
||||
});
|
||||
|
||||
setStatusAndNotifyEvent(ChainStatus.FAILED);
|
||||
eventManager.notifyChainError(throwable, this);
|
||||
return ChainStatus.FAILED;
|
||||
}
|
||||
|
||||
public void suspend() {
|
||||
setStatusAndNotifyEvent(ChainStatus.SUSPEND);
|
||||
}
|
||||
|
||||
public void suspend(Node node) {
|
||||
updateStateSafely(state -> {
|
||||
state.addSuspendNodeId(node.getId());
|
||||
return EnumSet.of(ChainStateField.SUSPEND_NODE_IDS);
|
||||
});
|
||||
setStatusAndNotifyEvent(ChainStatus.SUSPEND);
|
||||
}
|
||||
|
||||
|
||||
public void resume(Map<String, Object> variables) {
|
||||
ChainState newState = updateStateSafely(state -> {
|
||||
if (variables != null) {
|
||||
state.getMemory().putAll(variables);
|
||||
return EnumSet.of(ChainStateField.MEMORY);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
notifyEvent(new ChainResumeEvent(this, variables));
|
||||
setStatusAndNotifyEvent(ChainStatus.RUNNING);
|
||||
|
||||
Set<String> suspendNodeIds = newState.getSuspendNodeIds();
|
||||
if (suspendNodeIds != null && !suspendNodeIds.isEmpty()) {
|
||||
// 移除 suspend 状态,方便二次 suspend 时,不带有旧数据
|
||||
updateStateSafely(state -> {
|
||||
state.setSuspendNodeIds(null);
|
||||
state.setSuspendForParameters(null);
|
||||
return EnumSet.of(ChainStateField.SUSPEND_NODE_IDS, ChainStateField.SUSPEND_FOR_PARAMETERS);
|
||||
});
|
||||
|
||||
for (String id : suspendNodeIds) {
|
||||
Node node = definition.getNodeById(id);
|
||||
if (node == null) {
|
||||
throw new ChainException("Node not found: " + id);
|
||||
}
|
||||
scheduleNode(node, null, TriggerType.RESUME, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
resume(Collections.emptyMap());
|
||||
}
|
||||
|
||||
public void output(Node node, Object response) {
|
||||
eventManager.notifyOutput(this, node, response);
|
||||
}
|
||||
|
||||
|
||||
public EventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
|
||||
public void setEventManager(EventManager eventManager) {
|
||||
this.eventManager = eventManager;
|
||||
}
|
||||
|
||||
public ChainStateRepository getChainStateRepository() {
|
||||
return chainStateRepository;
|
||||
}
|
||||
|
||||
public void setChainStateRepository(ChainStateRepository chainStateRepository) {
|
||||
this.chainStateRepository = chainStateRepository;
|
||||
}
|
||||
|
||||
public ChainDefinition getDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
public TriggerScheduler getTriggerScheduler() {
|
||||
if (this.triggerScheduler == null) {
|
||||
this.triggerScheduler = ChainRuntime.triggerScheduler();
|
||||
}
|
||||
return triggerScheduler;
|
||||
}
|
||||
|
||||
public void setTriggerScheduler(TriggerScheduler triggerScheduler) {
|
||||
this.triggerScheduler = triggerScheduler;
|
||||
}
|
||||
|
||||
public NodeStateRepository getNodeStateRepository() {
|
||||
return nodeStateRepository;
|
||||
}
|
||||
|
||||
public void setNodeStateRepository(NodeStateRepository nodeStateRepository) {
|
||||
this.nodeStateRepository = nodeStateRepository;
|
||||
}
|
||||
|
||||
public String getStateInstanceId() {
|
||||
return stateInstanceId;
|
||||
}
|
||||
|
||||
public ChainState getState() {
|
||||
return chainStateRepository.load(stateInstanceId);
|
||||
}
|
||||
|
||||
public ChainState getState(String stateInstanceId) {
|
||||
return chainStateRepository.load(stateInstanceId);
|
||||
}
|
||||
|
||||
public void setStateInstanceId(String stateInstanceId) {
|
||||
this.stateInstanceId = stateInstanceId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
public class ChainConsts {
|
||||
|
||||
public static final String SCHEDULE_NEXT_NODE_DISABLED_KEY = "__schedule_next_node_disabled";
|
||||
public static final String NODE_STATE_STATUS_KEY = "__node_state_status";
|
||||
public static final String CHAIN_STATE_STATUS_KEY = "__chain_state_status";
|
||||
public static final String CHAIN_STATE_MESSAGE_KEY = "__chain_state_message";
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
public class ChainDefinition implements Serializable {
|
||||
protected String id;
|
||||
protected String name;
|
||||
protected String description;
|
||||
protected List<Node> nodes;
|
||||
protected List<Edge> edges;
|
||||
|
||||
public ChainDefinition() {
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<Node> getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void setNodes(List<Node> nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public List<Edge> getEdges() {
|
||||
return edges;
|
||||
}
|
||||
|
||||
public void setEdges(List<Edge> edges) {
|
||||
this.edges = edges;
|
||||
}
|
||||
|
||||
public List<Edge> getOutwardEdge(String nodeId) {
|
||||
List<Edge> result = new ArrayList<>();
|
||||
for (Edge edge : edges) {
|
||||
if (nodeId.equals(edge.getSource())) {
|
||||
result.add(edge);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public List<Edge> getInwardEdge(String nodeId) {
|
||||
List<Edge> result = new ArrayList<>();
|
||||
for (Edge edge : edges) {
|
||||
if (nodeId.equals(edge.getTarget())) {
|
||||
result.add(edge);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void addNode(Node node) {
|
||||
if (nodes == null) {
|
||||
this.nodes = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (StringUtil.noText(node.getId())) {
|
||||
node.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
nodes.add(node);
|
||||
|
||||
// if (this.edges != null) {
|
||||
// for (Edge edge : edges) {
|
||||
// if (node.getId().equals(edge.getSource())) {
|
||||
// node.addOutwardEdge(edge);
|
||||
// } else if (node.getId().equals(edge.getTarget())) {
|
||||
// node.addInwardEdge(edge);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
public Node getNodeById(String id) {
|
||||
if (id == null || StringUtil.noText(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Node node : this.nodes) {
|
||||
if (id.equals(node.getId())) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void addEdge(Edge edge) {
|
||||
if (this.edges == null) {
|
||||
this.edges = new ArrayList<>();
|
||||
}
|
||||
this.edges.add(edge);
|
||||
|
||||
// boolean findSource = false, findTarget = false;
|
||||
// for (Node node : this.nodes) {
|
||||
// if (node.getId().equals(edge.getSource())) {
|
||||
// node.addOutwardEdge(edge);
|
||||
// findSource = true;
|
||||
// } else if (node.getId().equals(edge.getTarget())) {
|
||||
// node.addInwardEdge(edge);
|
||||
// findTarget = true;
|
||||
// }
|
||||
// if (findSource && findTarget) {
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
public Edge getEdgeById(String edgeId) {
|
||||
for (Edge edge : this.edges) {
|
||||
if (edgeId.equals(edge.getId())) {
|
||||
return edge;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Node> getStartNodes() {
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Node> result = new ArrayList<>();
|
||||
|
||||
for (Node node : nodes) {
|
||||
// if (CollectionUtil.noItems(node.getInwardEdges())) {
|
||||
// result.add(node);
|
||||
// }
|
||||
List<Edge> inwardEdge = getInwardEdge(node.getId());
|
||||
if (inwardEdge == null || inwardEdge.isEmpty()) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public List<Parameter> getStartParameters() {
|
||||
List<Node> startNodes = this.getStartNodes();
|
||||
if (startNodes == null || startNodes.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Parameter> parameters = new ArrayList<>();
|
||||
for (Node node : startNodes) {
|
||||
List<Parameter> nodeParameters = node.getParameters();
|
||||
if (nodeParameters != null) parameters.addAll(nodeParameters);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainDefinition{" +
|
||||
"id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", nodes=" + nodes +
|
||||
", edges=" + edges +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
public class ChainException extends RuntimeException {
|
||||
/**
|
||||
* Constructs a new runtime exception with {@code null} as its
|
||||
* detail message. The cause is not initialized, and may subsequently be
|
||||
* initialized by a call to {@link #initCause}.
|
||||
*/
|
||||
public ChainException() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message.
|
||||
* The cause is not initialized, and may subsequently be initialized by a
|
||||
* call to {@link #initCause}.
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for
|
||||
* later retrieval by the {@link #getMessage()} method.
|
||||
*/
|
||||
public ChainException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message and
|
||||
* cause. <p>Note that the detail message associated with
|
||||
* {@code cause} is <i>not</i> automatically incorporated in
|
||||
* this runtime exception's detail message.
|
||||
*
|
||||
* @param message the detail message (which is saved for later retrieval
|
||||
* by the {@link #getMessage()} method).
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @since 1.4
|
||||
*/
|
||||
public ChainException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified cause and a
|
||||
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
|
||||
* (which typically contains the class and detail message of
|
||||
* <tt>cause</tt>). This constructor is useful for runtime exceptions
|
||||
* that are little more than wrappers for other throwables.
|
||||
*
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @since 1.4
|
||||
*/
|
||||
public ChainException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail
|
||||
* message, cause, suppression enabled or disabled, and writable
|
||||
* stack trace enabled or disabled.
|
||||
*
|
||||
* @param message the detail message.
|
||||
* @param cause the cause. (A {@code null} value is permitted,
|
||||
* and indicates that the cause is nonexistent or unknown.)
|
||||
* @param enableSuppression whether or not suppression is enabled
|
||||
* or disabled
|
||||
* @param writableStackTrace whether or not the stack trace should
|
||||
* be writable
|
||||
* @since 1.7
|
||||
*/
|
||||
public ChainException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.parser.DefaultJSONParser;
|
||||
import com.alibaba.fastjson.parser.Feature;
|
||||
import com.alibaba.fastjson.parser.ParserConfig;
|
||||
import com.alibaba.fastjson.parser.deserializer.ObjectDeserializer;
|
||||
import com.alibaba.fastjson.serializer.JSONSerializer;
|
||||
import com.alibaba.fastjson.serializer.ObjectSerializer;
|
||||
import com.alibaba.fastjson.serializer.SerializeConfig;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.easyagents.flow.core.util.MapUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.util.TextTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ChainState implements Serializable {
|
||||
|
||||
private String instanceId;
|
||||
private String parentInstanceId;
|
||||
private String chainDefinitionId;
|
||||
private ConcurrentHashMap<String, Object> memory = new ConcurrentHashMap<>();
|
||||
|
||||
private Map<String, Object> executeResult;
|
||||
private Map<String, Object> environment;
|
||||
|
||||
private List<String> triggerEdgeIds;
|
||||
private List<String> triggerNodeIds;
|
||||
|
||||
private List<String> uncheckedEdgeIds;
|
||||
private List<String> uncheckedNodeIds;
|
||||
|
||||
// 算力消耗定义,积分消耗
|
||||
private long computeCost;
|
||||
private Set<String> suspendNodeIds;
|
||||
private List<Parameter> suspendForParameters;
|
||||
private ChainStatus status;
|
||||
private String message;
|
||||
private ExceptionSummary error;
|
||||
private long version;
|
||||
|
||||
public ChainState() {
|
||||
this.instanceId = UUID.randomUUID().toString();
|
||||
this.status = ChainStatus.READY;
|
||||
this.computeCost = 0;
|
||||
}
|
||||
|
||||
public String getInstanceId() {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
public void setInstanceId(String instanceId) {
|
||||
this.instanceId = instanceId;
|
||||
}
|
||||
|
||||
public String getParentInstanceId() {
|
||||
return parentInstanceId;
|
||||
}
|
||||
|
||||
public void setParentInstanceId(String parentInstanceId) {
|
||||
this.parentInstanceId = parentInstanceId;
|
||||
}
|
||||
|
||||
public String getChainDefinitionId() {
|
||||
return chainDefinitionId;
|
||||
}
|
||||
|
||||
public void setChainDefinitionId(String chainDefinitionId) {
|
||||
this.chainDefinitionId = chainDefinitionId;
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, Object> getMemory() {
|
||||
return memory;
|
||||
}
|
||||
|
||||
public void setMemory(ConcurrentHashMap<String, Object> memory) {
|
||||
this.memory = memory;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExecuteResult() {
|
||||
return executeResult;
|
||||
}
|
||||
|
||||
public void setExecuteResult(Map<String, Object> executeResult) {
|
||||
this.executeResult = executeResult;
|
||||
}
|
||||
|
||||
public Map<String, Object> getEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
public void setEnvironment(Map<String, Object> environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
|
||||
public List<String> getTriggerEdgeIds() {
|
||||
return triggerEdgeIds;
|
||||
}
|
||||
|
||||
public void setTriggerEdgeIds(List<String> triggerEdgeIds) {
|
||||
this.triggerEdgeIds = triggerEdgeIds;
|
||||
}
|
||||
|
||||
public void addTriggerEdgeId(String edgeId) {
|
||||
if (triggerEdgeIds == null) {
|
||||
triggerEdgeIds = new ArrayList<>();
|
||||
}
|
||||
triggerEdgeIds.add(edgeId);
|
||||
}
|
||||
|
||||
public List<String> getTriggerNodeIds() {
|
||||
return triggerNodeIds;
|
||||
}
|
||||
|
||||
public void setTriggerNodeIds(List<String> triggerNodeIds) {
|
||||
this.triggerNodeIds = triggerNodeIds;
|
||||
}
|
||||
|
||||
public void addTriggerNodeId(String nodeId) {
|
||||
if (triggerNodeIds == null) {
|
||||
triggerNodeIds = new ArrayList<>();
|
||||
}
|
||||
triggerNodeIds.add(nodeId);
|
||||
}
|
||||
|
||||
public List<String> getUncheckedEdgeIds() {
|
||||
return uncheckedEdgeIds;
|
||||
}
|
||||
|
||||
public void setUncheckedEdgeIds(List<String> uncheckedEdgeIds) {
|
||||
this.uncheckedEdgeIds = uncheckedEdgeIds;
|
||||
}
|
||||
|
||||
public void addUncheckedEdgeId(String edgeId) {
|
||||
if (uncheckedEdgeIds == null) {
|
||||
uncheckedEdgeIds = new ArrayList<>();
|
||||
}
|
||||
uncheckedEdgeIds.add(edgeId);
|
||||
}
|
||||
|
||||
public boolean removeUncheckedEdgeId(String edgeId) {
|
||||
if (uncheckedEdgeIds == null) {
|
||||
return false;
|
||||
}
|
||||
return uncheckedEdgeIds.remove(edgeId);
|
||||
}
|
||||
|
||||
public List<String> getUncheckedNodeIds() {
|
||||
return uncheckedNodeIds;
|
||||
}
|
||||
|
||||
public void setUncheckedNodeIds(List<String> uncheckedNodeIds) {
|
||||
this.uncheckedNodeIds = uncheckedNodeIds;
|
||||
}
|
||||
|
||||
public void addUncheckedNodeId(String nodeId) {
|
||||
if (uncheckedNodeIds == null) {
|
||||
uncheckedNodeIds = new ArrayList<>();
|
||||
}
|
||||
uncheckedNodeIds.add(nodeId);
|
||||
}
|
||||
|
||||
public boolean removeUncheckedNodeId(String nodeId) {
|
||||
if (uncheckedNodeIds == null) {
|
||||
return false;
|
||||
}
|
||||
return uncheckedNodeIds.remove(nodeId);
|
||||
}
|
||||
|
||||
public Long getComputeCost() {
|
||||
return computeCost;
|
||||
}
|
||||
|
||||
public void setComputeCost(Long computeCost) {
|
||||
this.computeCost = computeCost;
|
||||
}
|
||||
|
||||
public void setComputeCost(long computeCost) {
|
||||
this.computeCost = computeCost;
|
||||
}
|
||||
|
||||
public Set<String> getSuspendNodeIds() {
|
||||
return suspendNodeIds;
|
||||
}
|
||||
|
||||
public void setSuspendNodeIds(Set<String> suspendNodeIds) {
|
||||
this.suspendNodeIds = suspendNodeIds;
|
||||
}
|
||||
|
||||
public void addSuspendNodeId(String nodeId) {
|
||||
if (suspendNodeIds == null) {
|
||||
suspendNodeIds = new HashSet<>();
|
||||
}
|
||||
suspendNodeIds.add(nodeId);
|
||||
}
|
||||
|
||||
public void removeSuspendNodeId(String nodeId) {
|
||||
if (suspendNodeIds == null) {
|
||||
return;
|
||||
}
|
||||
suspendNodeIds.remove(nodeId);
|
||||
}
|
||||
|
||||
public List<Parameter> getSuspendForParameters() {
|
||||
return suspendForParameters;
|
||||
}
|
||||
|
||||
public void setSuspendForParameters(List<Parameter> suspendForParameters) {
|
||||
this.suspendForParameters = suspendForParameters;
|
||||
}
|
||||
|
||||
public void addSuspendForParameter(Parameter parameter) {
|
||||
if (suspendForParameters == null) {
|
||||
suspendForParameters = new ArrayList<>();
|
||||
}
|
||||
suspendForParameters.add(parameter);
|
||||
}
|
||||
|
||||
public void addSuspendForParameters(List<Parameter> parameters) {
|
||||
if (parameters == null) {
|
||||
return;
|
||||
}
|
||||
if (suspendForParameters == null) {
|
||||
suspendForParameters = new ArrayList<>();
|
||||
}
|
||||
suspendForParameters.addAll(parameters);
|
||||
}
|
||||
|
||||
public ChainStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ChainStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ExceptionSummary getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(ExceptionSummary error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public static ChainState fromJSON(String jsonString) {
|
||||
ParserConfig config = new ParserConfig();
|
||||
config.putDeserializer(ChainState.class, new ChainDeserializer());
|
||||
return JSON.parseObject(jsonString, ChainState.class, config, Feature.SupportAutoType);
|
||||
}
|
||||
|
||||
public String toJSON() {
|
||||
SerializeConfig config = new SerializeConfig();
|
||||
config.put(ChainState.class, new ChainSerializer());
|
||||
return JSON.toJSONString(this, config, SerializerFeature.WriteClassName);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.instanceId = null;
|
||||
this.chainDefinitionId = null;
|
||||
this.memory.clear();
|
||||
this.executeResult = null;
|
||||
this.environment = null;
|
||||
this.computeCost = 0;
|
||||
this.suspendNodeIds = null;
|
||||
this.suspendForParameters = null;
|
||||
this.status = ChainStatus.READY;
|
||||
this.message = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
|
||||
public void addComputeCost(Long value) {
|
||||
if (value == null) {
|
||||
value = 0L;
|
||||
}
|
||||
this.computeCost += value;
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> getNodeExecuteResult(String nodeId) {
|
||||
if (memory == null || memory.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
memory.forEach((k, v) -> {
|
||||
if (k.startsWith(nodeId + ".")) {
|
||||
String newKey = k.substring(nodeId.length() + 1);
|
||||
result.put(newKey, v);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// public Map<String, Object> getTriggerVariables() {
|
||||
// Trigger trigger = TriggerContext.getCurrentTrigger();
|
||||
// if (trigger != null) {
|
||||
// return trigger.getVariables();
|
||||
// }
|
||||
// return Collections.emptyMap();
|
||||
// }
|
||||
|
||||
|
||||
public Object resolveValue(String path) {
|
||||
Object result = MapUtil.getByPath(getMemory(), path);
|
||||
if (result == null) result = MapUtil.getByPath(getEnvironment(), path);
|
||||
// if (result == null) result = MapUtil.getByPath(getTriggerVariables(), path);
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> resolveParameters(Node node) {
|
||||
return resolveParameters(node, node.getParameters());
|
||||
}
|
||||
|
||||
public Map<String, Object> resolveParameters(Node node, List<? extends Parameter> parameters) {
|
||||
return resolveParameters(node, parameters, null);
|
||||
}
|
||||
|
||||
public Map<String, Object> resolveParameters(Node node, List<? extends Parameter> parameters, Map<String, Object> formatArgs) {
|
||||
return resolveParameters(node, parameters, formatArgs, false);
|
||||
}
|
||||
|
||||
private boolean isNullOrBlank(Object value) {
|
||||
return value == null || value instanceof String && StringUtil.noText((String) value);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> getEnvMap() {
|
||||
Map<String, Object> formatArgsMap = new HashMap<>();
|
||||
formatArgsMap.put("env", getEnvironment());
|
||||
formatArgsMap.put("env.sys", System.getenv());
|
||||
return formatArgsMap;
|
||||
}
|
||||
|
||||
public Map<String, Object> resolveParameters(Node node, List<? extends Parameter> parameters, Map<String, Object> formatArgs, boolean ignoreRequired) {
|
||||
if (parameters == null || parameters.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> variables = new LinkedHashMap<>();
|
||||
List<Parameter> suspendParameters = null;
|
||||
for (Parameter parameter : parameters) {
|
||||
RefType refType = parameter.getRefType();
|
||||
Object value = null;
|
||||
if (refType == RefType.FIXED) {
|
||||
value = TextTemplate.of(parameter.getValue())
|
||||
.formatToString(Arrays.asList(formatArgs, getEnvMap()));
|
||||
} else if (refType == RefType.REF) {
|
||||
value = this.resolveValue(parameter.getRef());
|
||||
}
|
||||
// 单节点执行时,参数只会传入 name 内容。
|
||||
if (value == null) {
|
||||
value = this.resolveValue(parameter.getName());
|
||||
}
|
||||
|
||||
if (value == null && parameter.getDefaultValue() != null) {
|
||||
value = parameter.getDefaultValue();
|
||||
}
|
||||
|
||||
if (refType == RefType.INPUT && isNullOrBlank(value)) {
|
||||
if (!ignoreRequired && parameter.isRequired()) {
|
||||
if (suspendParameters == null) {
|
||||
suspendParameters = new ArrayList<>();
|
||||
}
|
||||
suspendParameters.add(parameter);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (parameter.isRequired() && isNullOrBlank(value)) {
|
||||
if (!ignoreRequired) {
|
||||
throw new ChainException(node.getName() + " Missing required parameter:" + parameter.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
value = ((String) value).trim();
|
||||
if (parameter.getDataType() == DataType.Boolean) {
|
||||
value = "true".equalsIgnoreCase((String) value) || "1".equalsIgnoreCase((String) value);
|
||||
} else if (parameter.getDataType() == DataType.Number) {
|
||||
value = Long.parseLong((String) value);
|
||||
} else if (parameter.getDataType() == DataType.Array) {
|
||||
value = JSON.parseArray((String) value);
|
||||
}
|
||||
}
|
||||
|
||||
variables.put(parameter.getName(), value);
|
||||
}
|
||||
|
||||
if (suspendParameters != null && !suspendParameters.isEmpty()) {
|
||||
// 构建参数名称列表
|
||||
String missingParams = suspendParameters.stream()
|
||||
.map(Parameter::getName)
|
||||
.collect(Collectors.joining("', '", "'", "'"));
|
||||
|
||||
String errorMessage = String.format(
|
||||
"Node '%s' (type: %s) is suspended. Waiting for input parameters: %s.",
|
||||
StringUtil.getFirstWithText(node.getName(), node.getId()),
|
||||
node.getClass().getSimpleName(),
|
||||
missingParams
|
||||
);
|
||||
|
||||
throw new ChainSuspendException(errorMessage, suspendParameters);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
|
||||
public static class ChainSerializer implements ObjectSerializer {
|
||||
@Override
|
||||
public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
|
||||
if (object == null) {
|
||||
serializer.writeNull();
|
||||
return;
|
||||
}
|
||||
ChainState chain = (ChainState) object;
|
||||
serializer.write(chain.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChainDeserializer implements ObjectDeserializer {
|
||||
@Override
|
||||
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
|
||||
String value = parser.parseObject(String.class);
|
||||
//noinspection unchecked
|
||||
return (T) ChainState.fromJSON(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainState{" +
|
||||
"instanceId='" + instanceId + '\'' +
|
||||
", chainDefinitionId='" + chainDefinitionId + '\'' +
|
||||
", memory=" + memory +
|
||||
", executeResult=" + executeResult +
|
||||
", environment=" + environment +
|
||||
", computeCost=" + computeCost +
|
||||
", suspendNodeIds=" + suspendNodeIds +
|
||||
", suspendForParameters=" + suspendForParameters +
|
||||
", status=" + status +
|
||||
", message='" + message + '\'' +
|
||||
", error=" + error +
|
||||
", version=" + version +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
/**
|
||||
* 链(Chain)执行生命周期状态枚举
|
||||
* <p>
|
||||
* 该状态机描述了一个 Chain 实例从创建到终止的完整生命周期。
|
||||
* 状态分为三类:
|
||||
* - <b>初始状态</b>:READY
|
||||
* - <b>运行中状态</b>:RUNNING, SUSPEND, WAITING
|
||||
* - <b>终态(Terminal)</b>:SUCCEEDED, FAILED, CANCELLED(不可再变更)
|
||||
* <p>
|
||||
* 设计原则:
|
||||
* - 使用行业通用术语(如 SUCCEEDED/FAILED,而非 FINISHED_NORMAL/ABNORMAL)
|
||||
* - 明确区分人工干预(SUSPEND)与系统调度(WAITING)
|
||||
* - 终态互斥且不可逆,便于状态判断与持久化恢复
|
||||
*/
|
||||
public enum ChainStatus {
|
||||
|
||||
/**
|
||||
* 初始状态:Chain 已创建,但尚未开始执行。
|
||||
* <p>
|
||||
* 此状态下,Chain 的内存为空,节点尚未触发。
|
||||
* 调用 {@link Chain#execute} 或 {@link Chain#doExecute} 后进入 {@link #RUNNING}。
|
||||
*/
|
||||
READY(0),
|
||||
|
||||
/**
|
||||
* 运行中:Chain 正在执行节点逻辑(同步或异步)。
|
||||
* <p>
|
||||
* 只要至少一个节点仍在处理(包括等待 Phaser 同步),状态即为 RUNNING。
|
||||
* 遇到挂起条件(如缺少参数、loop 间隔)时,会转为 {@link #SUSPEND}
|
||||
*/
|
||||
RUNNING(1),
|
||||
|
||||
/**
|
||||
* 暂停(人工干预):Chain 因缺少外部输入而暂停,需用户主动恢复。
|
||||
* <p>
|
||||
* 典型场景:
|
||||
* - 节点参数缺失且标记为 required(等待用户提交)
|
||||
* - 人工审批节点(等待管理员操作)
|
||||
* <p>
|
||||
* 恢复方式:调用 {@link Chain#resume(Map)} 注入所需变量。
|
||||
* 监听器:通过 {@link com.easyagents.flow.core.chain.listener.ChainSuspendListener} 感知。
|
||||
*/
|
||||
SUSPEND(5),
|
||||
|
||||
|
||||
/**
|
||||
* 错误(中间状态):执行中发生异常,但尚未终结(例如正在重试)。
|
||||
* <p>
|
||||
* 此状态表示:Chain 遇到错误,但仍在尝试恢复(如重试机制触发)。
|
||||
* 如果重试成功,可回到 RUNNING;如果重试耗尽,则进入 {@link #FAILED}。
|
||||
* <p>
|
||||
* ⚠️ 注意:此状态 <b>不是终态</b>,Chain 仍可恢复。
|
||||
*/
|
||||
ERROR(10),
|
||||
|
||||
/**
|
||||
* 成功完成:Chain 所有节点正常执行完毕,无错误发生。
|
||||
* <p>
|
||||
* 终态(Terminal State)—— 状态不可再变更。
|
||||
* 此状态下,Chain 的执行结果(executeResult)有效。
|
||||
*/
|
||||
SUCCEEDED(20),
|
||||
|
||||
/**
|
||||
* 失败结束:Chain 因未处理的异常或错误条件而终止。
|
||||
* <p>
|
||||
* 终态(Terminal State)—— 状态不可再变更。
|
||||
* 常见原因:节点抛出异常、重试耗尽、条件校验失败等。
|
||||
* 错误详情可通过 {@link ChainState#getError()} 获取。
|
||||
*/
|
||||
FAILED(21),
|
||||
|
||||
/**
|
||||
* 已取消:Chain 被用户或系统主动终止,非因错误。
|
||||
* <p>
|
||||
* 终态(Terminal State)—— 状态不可再变更。
|
||||
* 典型场景:
|
||||
* - 用户点击“取消”按钮
|
||||
* - 超时自动取消(如审批超时)
|
||||
* - 父流程终止子流程
|
||||
* <p>
|
||||
* 与 {@link #FAILED} 的区别:CANCELLED 是预期行为,通常不计入错误率。
|
||||
*/
|
||||
CANCELLED(22);
|
||||
|
||||
/**
|
||||
* 状态的数值标识,可用于数据库存储或网络传输
|
||||
*/
|
||||
private final int value;
|
||||
|
||||
ChainStatus(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前状态是否为终态(Terminal State)
|
||||
* <p>
|
||||
* 终态包括:SUCCEEDED, FAILED, CANCELLED
|
||||
* 一旦进入终态,Chain 不可再恢复或继续执行。
|
||||
*
|
||||
* @return 如果是终态,返回 true;否则返回 false
|
||||
*/
|
||||
public boolean isTerminal() {
|
||||
return this == SUCCEEDED || this == FAILED || this == CANCELLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前状态是否表示成功完成
|
||||
*
|
||||
* @return 如果是 {@link #SUCCEEDED},返回 true;否则返回 false
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return this == SUCCEEDED;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取状态对应的数值标识
|
||||
*
|
||||
* @return 状态值
|
||||
*/
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static ChainStatus fromValue(int value) {
|
||||
for (ChainStatus status : values()) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ChainSuspendException extends RuntimeException {
|
||||
|
||||
private final List<Parameter> suspendParameters;
|
||||
|
||||
public ChainSuspendException(String message, List<Parameter> suspendParameters) {
|
||||
super(message);
|
||||
this.suspendParameters = suspendParameters;
|
||||
}
|
||||
|
||||
public List<Parameter> getSuspendParameters() {
|
||||
return suspendParameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
public class ChainUpdateTimeoutException extends RuntimeException{
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with {@code null} as its
|
||||
* detail message. The cause is not initialized, and may subsequently be
|
||||
* initialized by a call to {@link #initCause}.
|
||||
*/
|
||||
public ChainUpdateTimeoutException() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message.
|
||||
* The cause is not initialized, and may subsequently be initialized by a
|
||||
* call to {@link #initCause}.
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for
|
||||
* later retrieval by the {@link #getMessage()} method.
|
||||
*/
|
||||
public ChainUpdateTimeoutException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message and
|
||||
* cause. <p>Note that the detail message associated with
|
||||
* {@code cause} is <i>not</i> automatically incorporated in
|
||||
* this runtime exception's detail message.
|
||||
*
|
||||
* @param message the detail message (which is saved for later retrieval
|
||||
* by the {@link #getMessage()} method).
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @since 1.4
|
||||
*/
|
||||
public ChainUpdateTimeoutException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified cause and a
|
||||
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
|
||||
* (which typically contains the class and detail message of
|
||||
* <tt>cause</tt>). This constructor is useful for runtime exceptions
|
||||
* that are little more than wrappers for other throwables.
|
||||
*
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @since 1.4
|
||||
*/
|
||||
public ChainUpdateTimeoutException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail
|
||||
* message, cause, suppression enabled or disabled, and writable
|
||||
* stack trace enabled or disabled.
|
||||
*
|
||||
* @param message the detail message.
|
||||
* @param cause the cause. (A {@code null} value is permitted,
|
||||
* and indicates that the cause is nonexistent or unknown.)
|
||||
* @param enableSuppression whether or not suppression is enabled
|
||||
* or disabled
|
||||
* @param writableStackTrace whether or not the stack trace should
|
||||
* be writable
|
||||
* @since 1.7
|
||||
*/
|
||||
public ChainUpdateTimeoutException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
public enum DataType {
|
||||
Array("Array"),
|
||||
Object("Object"),
|
||||
String("String"),
|
||||
Number("Number"),
|
||||
Boolean("Boolean"),
|
||||
File("File"),
|
||||
|
||||
Array_Object("Array<Object>"),
|
||||
Array_String("Array<String>"),
|
||||
Array_Number("Array<Number>"),
|
||||
Array_Boolean("Array<Boolean>"),
|
||||
Array_File("Array<File>"),
|
||||
;
|
||||
|
||||
private final String value;
|
||||
|
||||
DataType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static DataType ofValue(String value) {
|
||||
for (DataType type : DataType.values()) {
|
||||
if (type.value.equalsIgnoreCase(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
public class Edge {
|
||||
private String id;
|
||||
private String source;
|
||||
private String target;
|
||||
private EdgeCondition condition;
|
||||
|
||||
public Edge() {
|
||||
}
|
||||
|
||||
public Edge(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public String getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setTarget(String target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public EdgeCondition getCondition() {
|
||||
return condition;
|
||||
}
|
||||
|
||||
public void setCondition(EdgeCondition condition) {
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface EdgeCondition {
|
||||
|
||||
boolean check(Chain chain, Edge edge, Map<String, Object> executeResult);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
public interface Event {
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.easyagents.flow.core.chain.listener.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class EventManager {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EventManager.class);
|
||||
|
||||
protected final Map<Class<?>, List<ChainEventListener>> eventListeners = new ConcurrentHashMap<>();
|
||||
protected final List<ChainOutputListener> outputListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
protected final List<ChainErrorListener> chainErrorListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
protected final List<NodeErrorListener> nodeErrorListeners = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
/**
|
||||
* ---------- 通用事件监听器 ----------
|
||||
*/
|
||||
public void addEventListener(Class<? extends Event> eventClass, ChainEventListener listener) {
|
||||
eventListeners.computeIfAbsent(eventClass, k -> Collections.synchronizedList(new ArrayList<>())).add(listener);
|
||||
}
|
||||
|
||||
public void addEventListener(ChainEventListener listener) {
|
||||
addEventListener(Event.class, listener);
|
||||
}
|
||||
|
||||
public void removeEventListener(Class<? extends Event> eventClass, ChainEventListener listener) {
|
||||
List<ChainEventListener> list = eventListeners.get(eventClass);
|
||||
if (list != null) list.remove(listener);
|
||||
}
|
||||
|
||||
public void removeEventListener(ChainEventListener listener) {
|
||||
for (List<ChainEventListener> list : eventListeners.values()) {
|
||||
list.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyEvent(Event event, Chain chain) {
|
||||
for (Map.Entry<Class<?>, List<ChainEventListener>> entry : eventListeners.entrySet()) {
|
||||
if (entry.getKey().isInstance(event)) {
|
||||
for (ChainEventListener listener : entry.getValue()) {
|
||||
try {
|
||||
listener.onEvent(event, chain);
|
||||
} catch (Exception e) {
|
||||
log.error("Error in event listener: {}", e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ---------- Output Listener ----------
|
||||
*/
|
||||
public void addOutputListener(ChainOutputListener listener) {
|
||||
outputListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeOutputListener(ChainOutputListener listener) {
|
||||
outputListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void notifyOutput(Chain chain, Node node, Object response) {
|
||||
for (ChainOutputListener listener : outputListeners) {
|
||||
try {
|
||||
listener.onOutput(chain, node, response);
|
||||
} catch (Exception e) {
|
||||
log.error("Error in output listener: {}", e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ---------- Chain Error Listener ----------
|
||||
*/
|
||||
public void addChainErrorListener(ChainErrorListener listener) {
|
||||
chainErrorListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeChainErrorListener(ChainErrorListener listener) {
|
||||
chainErrorListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void notifyChainError(Throwable error, Chain chain) {
|
||||
for (ChainErrorListener listener : chainErrorListeners) {
|
||||
try {
|
||||
listener.onError(error, chain);
|
||||
} catch (Exception e) {
|
||||
log.error("Error in chain error listener: {}", e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ---------- Node Error Listener ----------
|
||||
*/
|
||||
public void addNodeErrorListener(NodeErrorListener listener) {
|
||||
nodeErrorListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeNodeErrorListener(NodeErrorListener listener) {
|
||||
nodeErrorListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void notifyNodeError(Throwable error, Node node, Map<String, Object> result, Chain chain) {
|
||||
for (NodeErrorListener listener : nodeErrorListeners) {
|
||||
try {
|
||||
listener.onError(error, node, result, chain);
|
||||
} catch (Exception e) {
|
||||
log.error("Error in node error listener: {}", e.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.Serializable;
|
||||
import java.io.StringWriter;
|
||||
|
||||
public class ExceptionSummary implements Serializable {
|
||||
|
||||
private String exceptionClass;
|
||||
private String message;
|
||||
private String stackTrace;
|
||||
|
||||
private String rootCauseClass;
|
||||
private String rootCauseMessage;
|
||||
|
||||
private String chainId;
|
||||
private String nodeId;
|
||||
|
||||
private String errorCode; // 可选
|
||||
|
||||
private long timestamp;
|
||||
|
||||
public ExceptionSummary(Throwable error) {
|
||||
this.exceptionClass = error.getClass().getName();
|
||||
this.message = error.getMessage();
|
||||
this.stackTrace = getStackTraceAsString(error);
|
||||
|
||||
Throwable root = getRootCause(error);
|
||||
this.rootCauseClass = root.getClass().getName();
|
||||
this.rootCauseMessage = root.getMessage();
|
||||
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private static Throwable getRootCause(Throwable t) {
|
||||
Throwable result = t;
|
||||
while (result.getCause() != null) {
|
||||
result = result.getCause();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getStackTraceAsString(Throwable t) {
|
||||
StringWriter sw = new StringWriter();
|
||||
PrintWriter pw = new PrintWriter(sw);
|
||||
t.printStackTrace(pw);
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
public String getExceptionClass() {
|
||||
return exceptionClass;
|
||||
}
|
||||
|
||||
public void setExceptionClass(String exceptionClass) {
|
||||
this.exceptionClass = exceptionClass;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getStackTrace() {
|
||||
return stackTrace;
|
||||
}
|
||||
|
||||
public void setStackTrace(String stackTrace) {
|
||||
this.stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
public String getRootCauseClass() {
|
||||
return rootCauseClass;
|
||||
}
|
||||
|
||||
public void setRootCauseClass(String rootCauseClass) {
|
||||
this.rootCauseClass = rootCauseClass;
|
||||
}
|
||||
|
||||
public String getRootCauseMessage() {
|
||||
return rootCauseMessage;
|
||||
}
|
||||
|
||||
public void setRootCauseMessage(String rootCauseMessage) {
|
||||
this.rootCauseMessage = rootCauseMessage;
|
||||
}
|
||||
|
||||
public String getChainId() {
|
||||
return chainId;
|
||||
}
|
||||
|
||||
public void setChainId(String chainId) {
|
||||
this.chainId = chainId;
|
||||
}
|
||||
|
||||
public String getNodeId() {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
public void setNodeId(String nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.easyagents.flow.core.util.JsConditionUtil;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class JsCodeCondition implements NodeCondition, EdgeCondition {
|
||||
private String code;
|
||||
|
||||
public JsCodeCondition() {
|
||||
}
|
||||
|
||||
public JsCodeCondition(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(Chain chain, Edge edge, Map<String, Object> executeResult) {
|
||||
Maps map = Maps.of("_edge", edge).set("_chain", chain);
|
||||
if (executeResult != null) {
|
||||
map.putAll(executeResult);
|
||||
}
|
||||
return JsConditionUtil.eval(code, chain, map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(Chain chain, NodeState state, Map<String, Object> executeResult) {
|
||||
Maps map = Maps.of("_state", state).set("_chain", chain);
|
||||
if (executeResult != null) {
|
||||
map.putAll(executeResult);
|
||||
}
|
||||
return JsConditionUtil.eval(code, chain, map);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.easyagents.flow.core.util.JsConditionUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class Node implements Serializable {
|
||||
private static final Logger log = org.slf4j.LoggerFactory.getLogger(Node.class);
|
||||
|
||||
protected String id;
|
||||
protected String parentId;
|
||||
protected String name;
|
||||
protected String description;
|
||||
|
||||
// protected List<Edge> inwardEdges;
|
||||
// protected List<Edge> outwardEdges;
|
||||
|
||||
protected NodeCondition condition;
|
||||
protected NodeValidator validator;
|
||||
|
||||
// 循环执行相关属性
|
||||
protected boolean loopEnable = false; // 是否启用循环执行
|
||||
protected long loopIntervalMs = 3000; // 循环间隔时间(毫秒)
|
||||
protected NodeCondition loopBreakCondition; // 跳出循环的条件
|
||||
protected int maxLoopCount = 0; // 0 表示不限制循环次数
|
||||
|
||||
protected boolean retryEnable = false;
|
||||
protected boolean resetRetryCountAfterNormal = false;
|
||||
protected int maxRetryCount = 0;
|
||||
protected long retryIntervalMs = 3000;
|
||||
|
||||
// 算力消耗定义,积分消耗
|
||||
protected String computeCostExpr;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(String parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
// public List<Edge> getInwardEdges() {
|
||||
// return inwardEdges;
|
||||
// }
|
||||
//
|
||||
// public void setInwardEdges(List<Edge> inwardEdges) {
|
||||
// this.inwardEdges = inwardEdges;
|
||||
// }
|
||||
//
|
||||
// public List<Edge> getOutwardEdges() {
|
||||
//// return outwardEdges;
|
||||
// }
|
||||
//
|
||||
// public void setOutwardEdges(List<Edge> outwardEdges) {
|
||||
// this.outwardEdges = outwardEdges;
|
||||
// }
|
||||
|
||||
public NodeCondition getCondition() {
|
||||
return condition;
|
||||
}
|
||||
|
||||
public void setCondition(NodeCondition condition) {
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
public NodeValidator getValidator() {
|
||||
return validator;
|
||||
}
|
||||
|
||||
public void setValidator(NodeValidator validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
// protected void addOutwardEdge(Edge edge) {
|
||||
// if (this.outwardEdges == null) {
|
||||
// this.outwardEdges = new ArrayList<>();
|
||||
// }
|
||||
// this.outwardEdges.add(edge);
|
||||
// }
|
||||
//
|
||||
// protected void addInwardEdge(Edge edge) {
|
||||
// if (this.inwardEdges == null) {
|
||||
// this.inwardEdges = new ArrayList<>();
|
||||
// }
|
||||
// this.inwardEdges.add(edge);
|
||||
// }
|
||||
|
||||
public boolean isLoopEnable() {
|
||||
return loopEnable;
|
||||
}
|
||||
|
||||
public void setLoopEnable(boolean loopEnable) {
|
||||
this.loopEnable = loopEnable;
|
||||
}
|
||||
|
||||
public long getLoopIntervalMs() {
|
||||
return loopIntervalMs;
|
||||
}
|
||||
|
||||
public void setLoopIntervalMs(long loopIntervalMs) {
|
||||
this.loopIntervalMs = loopIntervalMs;
|
||||
}
|
||||
|
||||
public NodeCondition getLoopBreakCondition() {
|
||||
return loopBreakCondition;
|
||||
}
|
||||
|
||||
public void setLoopBreakCondition(NodeCondition loopBreakCondition) {
|
||||
this.loopBreakCondition = loopBreakCondition;
|
||||
}
|
||||
|
||||
public int getMaxLoopCount() {
|
||||
return maxLoopCount;
|
||||
}
|
||||
|
||||
public void setMaxLoopCount(int maxLoopCount) {
|
||||
this.maxLoopCount = maxLoopCount;
|
||||
}
|
||||
|
||||
public List<Parameter> getParameters() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isRetryEnable() {
|
||||
return retryEnable;
|
||||
}
|
||||
|
||||
public void setRetryEnable(boolean retryEnable) {
|
||||
this.retryEnable = retryEnable;
|
||||
}
|
||||
|
||||
public boolean isResetRetryCountAfterNormal() {
|
||||
return resetRetryCountAfterNormal;
|
||||
}
|
||||
|
||||
public void setResetRetryCountAfterNormal(boolean resetRetryCountAfterNormal) {
|
||||
this.resetRetryCountAfterNormal = resetRetryCountAfterNormal;
|
||||
}
|
||||
|
||||
public int getMaxRetryCount() {
|
||||
return maxRetryCount;
|
||||
}
|
||||
|
||||
public void setMaxRetryCount(int maxRetryCount) {
|
||||
this.maxRetryCount = maxRetryCount;
|
||||
}
|
||||
|
||||
public long getRetryIntervalMs() {
|
||||
return retryIntervalMs;
|
||||
}
|
||||
|
||||
public void setRetryIntervalMs(long retryIntervalMs) {
|
||||
this.retryIntervalMs = retryIntervalMs;
|
||||
}
|
||||
|
||||
public String getComputeCostExpr() {
|
||||
return computeCostExpr;
|
||||
}
|
||||
|
||||
public void setComputeCostExpr(String computeCostExpr) {
|
||||
if (computeCostExpr != null) {
|
||||
computeCostExpr = computeCostExpr.trim();
|
||||
}
|
||||
this.computeCostExpr = computeCostExpr;
|
||||
}
|
||||
|
||||
public NodeValidResult validate() throws Exception {
|
||||
return validator != null ? validator.validate(this) : NodeValidResult.ok();
|
||||
}
|
||||
|
||||
|
||||
public abstract Map<String, Object> execute(Chain chain);
|
||||
|
||||
public long calculateComputeCost(Chain chain, Map<String, Object> executeResult) {
|
||||
|
||||
if (StringUtil.noText(computeCostExpr)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (computeCostExpr.startsWith("{{") && computeCostExpr.endsWith("}}")) {
|
||||
String expr = computeCostExpr.substring(2, computeCostExpr.length() - 2);
|
||||
return doCalculateComputeCost(expr, chain, executeResult);
|
||||
} else {
|
||||
try {
|
||||
return Long.parseLong(computeCostExpr);
|
||||
} catch (NumberFormatException e) {
|
||||
log.error(e.toString(), e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected long doCalculateComputeCost(String expr, Chain chain, Map<String, Object> result) {
|
||||
// Map<String, Object> parameterValues = chain.getState().getParameterValuesOnly(this, this.getParameters(), null);
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(this, this.getParameters(), null,true);
|
||||
Map<String, Object> newMap = new HashMap<>(result);
|
||||
newMap.putAll(parameterValues);
|
||||
return JsConditionUtil.evalLong(expr, chain, newMap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface NodeCondition {
|
||||
|
||||
boolean check(Chain chain, NodeState context, Map<String, Object> executeResult);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NodeState implements Serializable {
|
||||
|
||||
private String nodeId;
|
||||
private String chainInstanceId;
|
||||
|
||||
protected ConcurrentHashMap<String, Object> memory = new ConcurrentHashMap<>();
|
||||
protected NodeStatus status = NodeStatus.READY;
|
||||
|
||||
private int retryCount = 0;
|
||||
private int loopCount = 0;
|
||||
|
||||
private AtomicInteger triggerCount = new AtomicInteger(0);
|
||||
private List<String> triggerEdgeIds = new ArrayList<>();
|
||||
|
||||
private AtomicInteger executeCount = new AtomicInteger(0);
|
||||
private List<String> executeEdgeIds = new ArrayList<>();
|
||||
|
||||
ExceptionSummary error;
|
||||
|
||||
private long version;
|
||||
|
||||
public NodeState() {
|
||||
}
|
||||
|
||||
public String getNodeId() {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
public void setNodeId(String nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public String getChainInstanceId() {
|
||||
return chainInstanceId;
|
||||
}
|
||||
|
||||
public void setChainInstanceId(String chainInstanceId) {
|
||||
this.chainInstanceId = chainInstanceId;
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, Object> getMemory() {
|
||||
return memory;
|
||||
}
|
||||
|
||||
public void setMemory(ConcurrentHashMap<String, Object> memory) {
|
||||
this.memory = memory;
|
||||
}
|
||||
|
||||
public void addMemory(String key, Object value) {
|
||||
memory.put(key, value);
|
||||
}
|
||||
|
||||
public <T> T getMemoryOrDefault(String key, T defaultValue) {
|
||||
Object value = memory.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
//noinspection unchecked
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
public NodeStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public int getRetryCount() {
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public void setRetryCount(int retryCount) {
|
||||
this.retryCount = retryCount;
|
||||
}
|
||||
|
||||
public int getLoopCount() {
|
||||
return loopCount;
|
||||
}
|
||||
|
||||
public void setLoopCount(int loopCount) {
|
||||
this.loopCount = loopCount;
|
||||
}
|
||||
|
||||
public AtomicInteger getTriggerCount() {
|
||||
return triggerCount;
|
||||
}
|
||||
|
||||
public void setTriggerCount(AtomicInteger triggerCount) {
|
||||
this.triggerCount = triggerCount;
|
||||
}
|
||||
|
||||
public List<String> getTriggerEdgeIds() {
|
||||
return triggerEdgeIds;
|
||||
}
|
||||
|
||||
public void setTriggerEdgeIds(List<String> triggerEdgeIds) {
|
||||
this.triggerEdgeIds = triggerEdgeIds;
|
||||
}
|
||||
|
||||
public AtomicInteger getExecuteCount() {
|
||||
return executeCount;
|
||||
}
|
||||
|
||||
public void setExecuteCount(AtomicInteger executeCount) {
|
||||
this.executeCount = executeCount;
|
||||
}
|
||||
|
||||
public List<String> getExecuteEdgeIds() {
|
||||
return executeEdgeIds;
|
||||
}
|
||||
|
||||
public void setExecuteEdgeIds(List<String> executeEdgeIds) {
|
||||
this.executeEdgeIds = executeEdgeIds;
|
||||
}
|
||||
|
||||
public ExceptionSummary getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(ExceptionSummary error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public boolean isUpstreamFullyExecuted() {
|
||||
ChainDefinition definition = Chain.currentChain().getDefinition();
|
||||
List<Edge> inwardEdges = definition.getInwardEdge(nodeId);
|
||||
if (inwardEdges == null || inwardEdges.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> shouldBeTriggerIds = inwardEdges.stream().map(Edge::getId).collect(Collectors.toList());
|
||||
List<String> triggerEdgeIds = this.triggerEdgeIds;
|
||||
return triggerEdgeIds.size() >= shouldBeTriggerIds.size()
|
||||
&& shouldBeTriggerIds.parallelStream().allMatch(triggerEdgeIds::contains);
|
||||
}
|
||||
|
||||
public void recordTrigger(String fromEdgeId) {
|
||||
triggerCount.incrementAndGet();
|
||||
if (fromEdgeId == null) {
|
||||
fromEdgeId = "none";
|
||||
}
|
||||
triggerEdgeIds.add(fromEdgeId);
|
||||
}
|
||||
|
||||
public void recordExecute(String fromEdgeId) {
|
||||
executeCount.incrementAndGet();
|
||||
if (fromEdgeId == null) {
|
||||
fromEdgeId = "none";
|
||||
}
|
||||
executeEdgeIds.add(fromEdgeId);
|
||||
}
|
||||
|
||||
public void setStatus(NodeStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getLastExecuteEdgeId() {
|
||||
if (!executeEdgeIds.isEmpty()) {
|
||||
return executeEdgeIds.get(executeEdgeIds.size() - 1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
public enum NodeStatus {
|
||||
READY(0), // 未开始执行
|
||||
RUNNING(1), // 已开始执行,执行中...
|
||||
SUSPEND(5),
|
||||
ERROR(10), //发生错误
|
||||
SUCCEEDED(20), //正常结束
|
||||
FAILED(21); //错误结束
|
||||
|
||||
final int value;
|
||||
|
||||
NodeStatus(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static NodeStatus fromValue(int value) {
|
||||
for (NodeStatus status : NodeStatus.values()) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 表示一个链式节点校验结果。
|
||||
* 包含校验是否成功、消息说明以及附加的详细信息(如失败字段、原因等)。
|
||||
* <p>
|
||||
* 实例是不可变的(immutable),线程安全。
|
||||
*/
|
||||
public class NodeValidResult implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final NodeValidResult SUCCESS = new NodeValidResult(true, null, null);
|
||||
public static final NodeValidResult FAILURE = new NodeValidResult(false, null, null);
|
||||
|
||||
private final boolean success;
|
||||
private final String message;
|
||||
private final Map<String, Object> details;
|
||||
|
||||
/**
|
||||
* 私有构造器,确保通过工厂方法创建实例。
|
||||
*/
|
||||
private NodeValidResult(boolean success, String message, Map<String, Object> details) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
// 防御性拷贝,防止外部修改
|
||||
this.details = details != null ? Collections.unmodifiableMap(new java.util.HashMap<>(details)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取校验是否成功。
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结果消息(可为 null)。
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细信息(如校验失败的字段、原因等),不可变 Map。
|
||||
* 如果无详情,则返回 null。
|
||||
*/
|
||||
public Map<String, Object> getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
// ------------------ 静态工厂方法 ------------------
|
||||
|
||||
/**
|
||||
* 创建一个成功的校验结果(无消息、无详情)。
|
||||
*/
|
||||
public static NodeValidResult ok() {
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个成功的校验结果,附带消息。
|
||||
*/
|
||||
public static NodeValidResult ok(String message) {
|
||||
return new NodeValidResult(true, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个成功的校验结果,附带消息和详情。
|
||||
*/
|
||||
public static NodeValidResult ok(String message, Map<String, Object> details) {
|
||||
return new NodeValidResult(true, message, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个成功的校验结果,支持键值对形式传入 details。
|
||||
* <p>
|
||||
* 示例:success("验证通过", "userId", 123, "role", "admin")
|
||||
*
|
||||
* @param message 消息
|
||||
* @param kvPairs 键值对(必须成对:key1, value1, key2, value2...)
|
||||
* @return ChainNodeValidResult
|
||||
* @throws IllegalArgumentException 如果 kvPairs 数量为奇数
|
||||
*/
|
||||
public static NodeValidResult ok(String message, Object... kvPairs) {
|
||||
Map<String, Object> details = toMapFromPairs(kvPairs);
|
||||
return new NodeValidResult(true, message, details);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建一个失败的校验结果(无消息、无详情)。
|
||||
*/
|
||||
public static NodeValidResult fail() {
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个失败的校验结果,仅包含消息。
|
||||
*/
|
||||
public static NodeValidResult fail(String message) {
|
||||
return new NodeValidResult(false, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个失败的校验结果,包含消息和详情。
|
||||
*/
|
||||
public static NodeValidResult fail(String message, Map<String, Object> details) {
|
||||
return new NodeValidResult(false, message, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个失败的校验结果,支持键值对形式传入 details。
|
||||
* <p>
|
||||
* 示例:fail("验证失败", "field", "email", "reason", "格式错误")
|
||||
*/
|
||||
public static NodeValidResult fail(String message, Object... kvPairs) {
|
||||
Map<String, Object> details = toMapFromPairs(kvPairs);
|
||||
return new NodeValidResult(false, message, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:创建包含字段错误的失败结果。
|
||||
* 适用于表单/参数校验场景。
|
||||
*
|
||||
* @param field 错误字段名
|
||||
* @param reason 错误原因
|
||||
* @return 失败结果
|
||||
*/
|
||||
public static NodeValidResult failOnField(String field, String reason) {
|
||||
Map<String, Object> details = Collections.singletonMap("fieldError", field + ": " + reason);
|
||||
return fail(reason, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷方法:基于布尔值返回成功或失败结果。
|
||||
*
|
||||
* @param condition 条件
|
||||
* @param messageIfFail 条件不满足时的消息
|
||||
* @return 根据条件返回对应结果
|
||||
*/
|
||||
public static NodeValidResult require(boolean condition, String messageIfFail) {
|
||||
return condition ? ok() : fail(messageIfFail);
|
||||
}
|
||||
|
||||
|
||||
private static Map<String, Object> toMapFromPairs(Object... kvPairs) {
|
||||
if (kvPairs == null || kvPairs.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (kvPairs.length % 2 != 0) {
|
||||
throw new IllegalArgumentException("kvPairs must be even-sized: key1, value1, key2, value2...");
|
||||
}
|
||||
|
||||
Map<String, Object> map = new java.util.HashMap<>();
|
||||
for (int i = 0; i < kvPairs.length; i += 2) {
|
||||
Object key = kvPairs[i];
|
||||
Object value = kvPairs[i + 1];
|
||||
|
||||
if (!(key instanceof String)) {
|
||||
throw new IllegalArgumentException("Key must be a String, but got: " + key);
|
||||
}
|
||||
|
||||
map.put((String) key, value);
|
||||
}
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof NodeValidResult)) return false;
|
||||
NodeValidResult that = (NodeValidResult) o;
|
||||
return success == that.success &&
|
||||
Objects.equals(message, that.message) &&
|
||||
Objects.equals(details, that.details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(success, message, details);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainNodeValidResult{" +
|
||||
"success=" + success +
|
||||
", message='" + message + '\'' +
|
||||
", details=" + details +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
public interface NodeValidator {
|
||||
NodeValidResult validate(Node node);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class Parameter implements Serializable, Cloneable {
|
||||
protected String id;
|
||||
protected String name;
|
||||
protected String description;
|
||||
protected DataType dataType = DataType.String;
|
||||
|
||||
/**
|
||||
* 数据类型:文字内容、图片、音频、视频、文件
|
||||
*/
|
||||
protected String contentType;
|
||||
protected String ref;
|
||||
protected RefType refType;
|
||||
protected String value;
|
||||
protected boolean required;
|
||||
protected String defaultValue;
|
||||
protected List<Parameter> children;
|
||||
|
||||
/**
|
||||
* 枚举值列表
|
||||
*/
|
||||
protected List<Object> enums;
|
||||
|
||||
/**
|
||||
* 用户输入的表单类型,例如:"input" "textarea" "select" "radio" "checkbox" 等等
|
||||
*/
|
||||
protected String formType;
|
||||
|
||||
/**
|
||||
* 用户界面上显示的提示文字,用于引导用户进行选择
|
||||
*/
|
||||
protected String formLabel;
|
||||
|
||||
|
||||
/**
|
||||
* 表单的提示文字,用于引导用户进行选择或填写
|
||||
*/
|
||||
protected String formPlaceholder;
|
||||
|
||||
/**
|
||||
* 用户界面上显示的描述文字,用于引导用户进行选择
|
||||
*/
|
||||
protected String formDescription;
|
||||
|
||||
/**
|
||||
* 表单的其他属性
|
||||
*/
|
||||
protected String formAttrs;
|
||||
|
||||
|
||||
public Parameter() {
|
||||
}
|
||||
|
||||
public Parameter(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Parameter(String name, DataType dataType) {
|
||||
this.name = name;
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
public Parameter(String name, boolean required) {
|
||||
this.name = name;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public Parameter(String name, DataType dataType, boolean required) {
|
||||
this.name = name;
|
||||
this.dataType = dataType;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public DataType getDataType() {
|
||||
return dataType;
|
||||
}
|
||||
|
||||
public void setDataType(DataType dataType) {
|
||||
this.dataType = dataType;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getRef() {
|
||||
return ref;
|
||||
}
|
||||
|
||||
public void setRef(String ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
public RefType getRefType() {
|
||||
return refType;
|
||||
}
|
||||
|
||||
public void setRefType(RefType refType) {
|
||||
this.refType = refType;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void setDefaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public List<Parameter> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<Parameter> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public void addChild(Parameter parameter) {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>();
|
||||
}
|
||||
children.add(parameter);
|
||||
}
|
||||
|
||||
public void addChildren(Collection<Parameter> parameters) {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>();
|
||||
}
|
||||
children.addAll(parameters);
|
||||
}
|
||||
|
||||
public List<Object> getEnums() {
|
||||
return enums;
|
||||
}
|
||||
|
||||
public void setEnums(List<Object> enums) {
|
||||
this.enums = enums;
|
||||
}
|
||||
|
||||
public void setEnumsObject(Object enumsObject) {
|
||||
if (enumsObject == null) {
|
||||
this.enums = null;
|
||||
} else if (enumsObject instanceof Collection) {
|
||||
this.enums = new ArrayList<>();
|
||||
this.enums.addAll((Collection<?>) enumsObject);
|
||||
} else if (enumsObject.getClass().isArray()) {
|
||||
this.enums = new ArrayList<>();
|
||||
this.enums.addAll(Arrays.asList((Object[]) enumsObject));
|
||||
} else {
|
||||
this.enums = new ArrayList<>(1);
|
||||
this.enums.add(enumsObject);
|
||||
}
|
||||
}
|
||||
|
||||
public String getFormType() {
|
||||
return formType;
|
||||
}
|
||||
|
||||
public void setFormType(String formType) {
|
||||
this.formType = formType;
|
||||
}
|
||||
|
||||
public String getFormLabel() {
|
||||
return formLabel;
|
||||
}
|
||||
|
||||
public void setFormLabel(String formLabel) {
|
||||
this.formLabel = formLabel;
|
||||
}
|
||||
|
||||
public String getFormPlaceholder() {
|
||||
return formPlaceholder;
|
||||
}
|
||||
|
||||
public void setFormPlaceholder(String formPlaceholder) {
|
||||
this.formPlaceholder = formPlaceholder;
|
||||
}
|
||||
|
||||
public String getFormDescription() {
|
||||
return formDescription;
|
||||
}
|
||||
|
||||
public void setFormDescription(String formDescription) {
|
||||
this.formDescription = formDescription;
|
||||
}
|
||||
|
||||
public String getFormAttrs() {
|
||||
return formAttrs;
|
||||
}
|
||||
|
||||
public void setFormAttrs(String formAttrs) {
|
||||
this.formAttrs = formAttrs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Parameter{" +
|
||||
"id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", dataType=" + dataType +
|
||||
", contentType='" + contentType + '\'' +
|
||||
", ref='" + ref + '\'' +
|
||||
", refType=" + refType +
|
||||
", value='" + value + '\'' +
|
||||
", required=" + required +
|
||||
", defaultValue='" + defaultValue + '\'' +
|
||||
", children=" + children +
|
||||
", enums=" + enums +
|
||||
", formType='" + formType + '\'' +
|
||||
", formLabel='" + formLabel + '\'' +
|
||||
", formPlaceholder='" + formPlaceholder + '\'' +
|
||||
", formDescription='" + formDescription + '\'' +
|
||||
", formAttrs='" + formAttrs + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public Parameter clone() {
|
||||
try {
|
||||
Parameter clone = (Parameter) super.clone();
|
||||
if (this.children != null) {
|
||||
clone.children = new ArrayList<>(this.children.size());
|
||||
for (Parameter child : this.children) {
|
||||
clone.children.add(child.clone()); // 递归克隆
|
||||
}
|
||||
}
|
||||
if (this.enums != null) {
|
||||
clone.enums = new ArrayList<>(this.enums.size());
|
||||
clone.enums.addAll(this.enums);
|
||||
}
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONType;
|
||||
|
||||
@JSONType(typeName = "RefType")
|
||||
public enum RefType {
|
||||
REF("ref"),
|
||||
FIXED("fixed"),
|
||||
INPUT("input"),
|
||||
;
|
||||
|
||||
private final String value;
|
||||
|
||||
RefType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static RefType ofValue(String value) {
|
||||
for (RefType type : RefType.values()) {
|
||||
if (type.value.equals(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Event;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
public class BaseEvent implements Event {
|
||||
|
||||
protected final Chain chain;
|
||||
|
||||
public BaseEvent(Chain chain) {
|
||||
this.chain = chain;
|
||||
}
|
||||
|
||||
public Chain getChain() {
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
public class ChainEndEvent extends BaseEvent {
|
||||
|
||||
public ChainEndEvent(Chain chain) {
|
||||
super(chain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainEndEvent{" +
|
||||
"chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ChainResumeEvent extends BaseEvent {
|
||||
|
||||
private final Map<String, Object> variables;
|
||||
|
||||
public ChainResumeEvent(Chain chain, Map<String, Object> variables) {
|
||||
super(chain);
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainResumeEvent{" +
|
||||
"variables=" + variables +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ChainStartEvent extends BaseEvent {
|
||||
|
||||
private final Map<String, Object> variables;
|
||||
|
||||
public ChainStartEvent(Chain chain, Map<String, Object> variables) {
|
||||
super(chain);
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainStartEvent{" +
|
||||
"variables=" + variables +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainStatus;
|
||||
|
||||
public class ChainStatusChangeEvent extends BaseEvent {
|
||||
|
||||
private final ChainStatus status;
|
||||
private final ChainStatus before;
|
||||
|
||||
public ChainStatusChangeEvent(Chain chain, ChainStatus status, ChainStatus before) {
|
||||
super(chain);
|
||||
this.status = status;
|
||||
this.before = before;
|
||||
}
|
||||
|
||||
public ChainStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public ChainStatus getBefore() {
|
||||
return before;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChainStatusChangeEvent{" +
|
||||
"status=" + status +
|
||||
", before=" + before +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Edge;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class EdgeConditionCheckFailedEvent extends BaseEvent {
|
||||
|
||||
private final Edge edge;
|
||||
private final Node node;
|
||||
private final Map<String, Object> nodeExecuteResult;
|
||||
|
||||
public EdgeConditionCheckFailedEvent(Chain chain, Edge edge, Node node, Map<String, Object> nodeExecuteResult) {
|
||||
super(chain);
|
||||
this.edge = edge;
|
||||
this.node = node;
|
||||
this.nodeExecuteResult = nodeExecuteResult;
|
||||
}
|
||||
|
||||
public Edge getEdge() {
|
||||
return edge;
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
public Map<String, Object> getNodeExecuteResult() {
|
||||
return nodeExecuteResult;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.runtime.Trigger;
|
||||
|
||||
public class EdgeTriggerEvent extends BaseEvent {
|
||||
|
||||
private final Trigger trigger;
|
||||
|
||||
public EdgeTriggerEvent(Chain chain, Trigger trigger) {
|
||||
super(chain);
|
||||
this.trigger = trigger;
|
||||
}
|
||||
|
||||
public Trigger getTrigger() {
|
||||
return trigger;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class NodeEndEvent extends BaseEvent {
|
||||
|
||||
private final Node node;
|
||||
private final Map<String, Object> result;
|
||||
private final Throwable error;
|
||||
|
||||
public NodeEndEvent(Chain chain, Node node, Map<String, Object> result, Throwable error) {
|
||||
super(chain);
|
||||
this.node = node;
|
||||
this.result = result;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
public Map<String, Object> getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Throwable getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeEndEvent{" +
|
||||
"node=" + node +
|
||||
", result=" + result +
|
||||
", error=" + error +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.event;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
|
||||
public class NodeStartEvent extends BaseEvent {
|
||||
|
||||
private final Node node;
|
||||
|
||||
public NodeStartEvent(Chain chain, Node node) {
|
||||
super(chain);
|
||||
this.node = node;
|
||||
}
|
||||
|
||||
public Node getNode() {
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeStartEvent{" +
|
||||
"node=" + node +
|
||||
", chain=" + chain +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.listener;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
public interface ChainErrorListener {
|
||||
void onError(Throwable error, Chain chain);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.listener;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Event;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
public interface ChainEventListener {
|
||||
void onEvent(Event event, Chain chain);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.listener;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
|
||||
public interface ChainOutputListener {
|
||||
void onOutput(Chain chain, Node node, Object outputMessage);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.listener;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface NodeErrorListener {
|
||||
void onError(Throwable error, Node node, Map<String, Object> nodeResult, Chain chain);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Chain 执行链
|
||||
*/
|
||||
package com.easyagents.flow.core.chain;
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
|
||||
public interface ChainDefinitionRepository {
|
||||
|
||||
ChainDefinition getChainDefinitionById(String id);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
/**
|
||||
* 分布式锁句柄,用于确保锁的正确释放。
|
||||
* 使用 try-with-resources 模式保证释放。
|
||||
*/
|
||||
public interface ChainLock extends AutoCloseable {
|
||||
/**
|
||||
* 锁是否成功获取(用于判断是否超时)
|
||||
*/
|
||||
boolean isAcquired();
|
||||
|
||||
/**
|
||||
* 释放锁(幂等)
|
||||
*/
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.ref.PhantomReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ChainLockTimeoutException extends RuntimeException{
|
||||
/**
|
||||
* Constructs a new runtime exception with {@code null} as its
|
||||
* detail message. The cause is not initialized, and may subsequently be
|
||||
* initialized by a call to {@link #initCause}.
|
||||
*/
|
||||
public ChainLockTimeoutException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message.
|
||||
* The cause is not initialized, and may subsequently be initialized by a
|
||||
* call to {@link #initCause}.
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for
|
||||
* later retrieval by the {@link #getMessage()} method.
|
||||
*/
|
||||
public ChainLockTimeoutException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail message and
|
||||
* cause. <p>Note that the detail message associated with
|
||||
* {@code cause} is <i>not</i> automatically incorporated in
|
||||
* this runtime exception's detail message.
|
||||
*
|
||||
* @param message the detail message (which is saved for later retrieval
|
||||
* by the {@link #getMessage()} method).
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @since 1.4
|
||||
*/
|
||||
public ChainLockTimeoutException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified cause and a
|
||||
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
|
||||
* (which typically contains the class and detail message of
|
||||
* <tt>cause</tt>). This constructor is useful for runtime exceptions
|
||||
* that are little more than wrappers for other throwables.
|
||||
*
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @since 1.4
|
||||
*/
|
||||
public ChainLockTimeoutException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new runtime exception with the specified detail
|
||||
* message, cause, suppression enabled or disabled, and writable
|
||||
* stack trace enabled or disabled.
|
||||
*
|
||||
* @param message the detail message.
|
||||
* @param cause the cause. (A {@code null} value is permitted,
|
||||
* and indicates that the cause is nonexistent or unknown.)
|
||||
* @param enableSuppression whether or not suppression is enabled
|
||||
* or disabled
|
||||
* @param writableStackTrace whether or not the stack trace should
|
||||
* be writable
|
||||
* @since 1.7
|
||||
*/
|
||||
protected ChainLockTimeoutException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the detail message string of this throwable.
|
||||
*
|
||||
* @return the detail message string of this {@code Throwable} instance
|
||||
* (which may be {@code null}).
|
||||
*/
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return super.getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a localized description of this throwable.
|
||||
* Subclasses may override this method in order to produce a
|
||||
* locale-specific message. For subclasses that do not override this
|
||||
* method, the default implementation returns the same result as
|
||||
* {@code getMessage()}.
|
||||
*
|
||||
* @return The localized description of this throwable.
|
||||
* @since JDK1.1
|
||||
*/
|
||||
@Override
|
||||
public String getLocalizedMessage() {
|
||||
return super.getLocalizedMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cause of this throwable or {@code null} if the
|
||||
* cause is nonexistent or unknown. (The cause is the throwable that
|
||||
* caused this throwable to get thrown.)
|
||||
*
|
||||
* <p>This implementation returns the cause that was supplied via one of
|
||||
* the constructors requiring a {@code Throwable}, or that was set after
|
||||
* creation with the {@link #initCause(Throwable)} method. While it is
|
||||
* typically unnecessary to override this method, a subclass can override
|
||||
* it to return a cause set by some other means. This is appropriate for
|
||||
* a "legacy chained throwable" that predates the addition of chained
|
||||
* exceptions to {@code Throwable}. Note that it is <i>not</i>
|
||||
* necessary to override any of the {@code PrintStackTrace} methods,
|
||||
* all of which invoke the {@code getCause} method to determine the
|
||||
* cause of a throwable.
|
||||
*
|
||||
* @return the cause of this throwable or {@code null} if the
|
||||
* cause is nonexistent or unknown.
|
||||
* @since 1.4
|
||||
*/
|
||||
@Override
|
||||
public synchronized Throwable getCause() {
|
||||
return super.getCause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the <i>cause</i> of this throwable to the specified value.
|
||||
* (The cause is the throwable that caused this throwable to get thrown.)
|
||||
*
|
||||
* <p>This method can be called at most once. It is generally called from
|
||||
* within the constructor, or immediately after creating the
|
||||
* throwable. If this throwable was created
|
||||
* with {@link #Throwable(Throwable)} or
|
||||
* {@link #Throwable(String, Throwable)}, this method cannot be called
|
||||
* even once.
|
||||
*
|
||||
* <p>An example of using this method on a legacy throwable type
|
||||
* without other support for setting the cause is:
|
||||
*
|
||||
* <pre>
|
||||
* try {
|
||||
* lowLevelOp();
|
||||
* } catch (LowLevelException le) {
|
||||
* throw (HighLevelException)
|
||||
* new HighLevelException().initCause(le); // Legacy constructor
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A {@code null} value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
* @return a reference to this {@code Throwable} instance.
|
||||
* @throws IllegalArgumentException if {@code cause} is this
|
||||
* throwable. (A throwable cannot be its own cause.)
|
||||
* @throws IllegalStateException if this throwable was
|
||||
* created with {@link #Throwable(Throwable)} or
|
||||
* {@link #Throwable(String, Throwable)}, or this method has already
|
||||
* been called on this throwable.
|
||||
* @since 1.4
|
||||
*/
|
||||
@Override
|
||||
public synchronized Throwable initCause(Throwable cause) {
|
||||
return super.initCause(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a short description of this throwable.
|
||||
* The result is the concatenation of:
|
||||
* <ul>
|
||||
* <li> the {@linkplain Class#getName() name} of the class of this object
|
||||
* <li> ": " (a colon and a space)
|
||||
* <li> the result of invoking this object's {@link #getLocalizedMessage}
|
||||
* method
|
||||
* </ul>
|
||||
* If {@code getLocalizedMessage} returns {@code null}, then just
|
||||
* the class name is returned.
|
||||
*
|
||||
* @return a string representation of this throwable.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints this throwable and its backtrace to the
|
||||
* standard error stream. This method prints a stack trace for this
|
||||
* {@code Throwable} object on the error output stream that is
|
||||
* the value of the field {@code System.err}. The first line of
|
||||
* output contains the result of the {@link #toString()} method for
|
||||
* this object. Remaining lines represent data previously recorded by
|
||||
* the method {@link #fillInStackTrace()}. The format of this
|
||||
* information depends on the implementation, but the following
|
||||
* example may be regarded as typical:
|
||||
* <blockquote><pre>
|
||||
* java.lang.NullPointerException
|
||||
* at MyClass.mash(MyClass.java:9)
|
||||
* at MyClass.crunch(MyClass.java:6)
|
||||
* at MyClass.main(MyClass.java:3)
|
||||
* </pre></blockquote>
|
||||
* This example was produced by running the program:
|
||||
* <pre>
|
||||
* class MyClass {
|
||||
* public static void main(String[] args) {
|
||||
* crunch(null);
|
||||
* }
|
||||
* static void crunch(int[] a) {
|
||||
* mash(a);
|
||||
* }
|
||||
* static void mash(int[] b) {
|
||||
* System.out.println(b[0]);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* The backtrace for a throwable with an initialized, non-null cause
|
||||
* should generally include the backtrace for the cause. The format
|
||||
* of this information depends on the implementation, but the following
|
||||
* example may be regarded as typical:
|
||||
* <pre>
|
||||
* HighLevelException: MidLevelException: LowLevelException
|
||||
* at Junk.a(Junk.java:13)
|
||||
* at Junk.main(Junk.java:4)
|
||||
* Caused by: MidLevelException: LowLevelException
|
||||
* at Junk.c(Junk.java:23)
|
||||
* at Junk.b(Junk.java:17)
|
||||
* at Junk.a(Junk.java:11)
|
||||
* ... 1 more
|
||||
* Caused by: LowLevelException
|
||||
* at Junk.e(Junk.java:30)
|
||||
* at Junk.d(Junk.java:27)
|
||||
* at Junk.c(Junk.java:21)
|
||||
* ... 3 more
|
||||
* </pre>
|
||||
* Note the presence of lines containing the characters {@code "..."}.
|
||||
* These lines indicate that the remainder of the stack trace for this
|
||||
* exception matches the indicated number of frames from the bottom of the
|
||||
* stack trace of the exception that was caused by this exception (the
|
||||
* "enclosing" exception). This shorthand can greatly reduce the length
|
||||
* of the output in the common case where a wrapped exception is thrown
|
||||
* from same method as the "causative exception" is caught. The above
|
||||
* example was produced by running the program:
|
||||
* <pre>
|
||||
* public class Junk {
|
||||
* public static void main(String args[]) {
|
||||
* try {
|
||||
* a();
|
||||
* } catch(HighLevelException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
* }
|
||||
* static void a() throws HighLevelException {
|
||||
* try {
|
||||
* b();
|
||||
* } catch(MidLevelException e) {
|
||||
* throw new HighLevelException(e);
|
||||
* }
|
||||
* }
|
||||
* static void b() throws MidLevelException {
|
||||
* c();
|
||||
* }
|
||||
* static void c() throws MidLevelException {
|
||||
* try {
|
||||
* d();
|
||||
* } catch(LowLevelException e) {
|
||||
* throw new MidLevelException(e);
|
||||
* }
|
||||
* }
|
||||
* static void d() throws LowLevelException {
|
||||
* e();
|
||||
* }
|
||||
* static void e() throws LowLevelException {
|
||||
* throw new LowLevelException();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class HighLevelException extends Exception {
|
||||
* HighLevelException(Throwable cause) { super(cause); }
|
||||
* }
|
||||
*
|
||||
* class MidLevelException extends Exception {
|
||||
* MidLevelException(Throwable cause) { super(cause); }
|
||||
* }
|
||||
*
|
||||
* class LowLevelException extends Exception {
|
||||
* }
|
||||
* </pre>
|
||||
* As of release 7, the platform supports the notion of
|
||||
* <i>suppressed exceptions</i> (in conjunction with the {@code
|
||||
* try}-with-resources statement). Any exceptions that were
|
||||
* suppressed in order to deliver an exception are printed out
|
||||
* beneath the stack trace. The format of this information
|
||||
* depends on the implementation, but the following example may be
|
||||
* regarded as typical:
|
||||
*
|
||||
* <pre>
|
||||
* Exception in thread "main" java.lang.Exception: Something happened
|
||||
* at Foo.bar(Foo.java:10)
|
||||
* at Foo.main(Foo.java:5)
|
||||
* Suppressed: Resource$CloseFailException: Resource ID = 0
|
||||
* at Resource.close(Resource.java:26)
|
||||
* at Foo.bar(Foo.java:9)
|
||||
* ... 1 more
|
||||
* </pre>
|
||||
* Note that the "... n more" notation is used on suppressed exceptions
|
||||
* just at it is used on causes. Unlike causes, suppressed exceptions are
|
||||
* indented beyond their "containing exceptions."
|
||||
*
|
||||
* <p>An exception can have both a cause and one or more suppressed
|
||||
* exceptions:
|
||||
* <pre>
|
||||
* Exception in thread "main" java.lang.Exception: Main block
|
||||
* at Foo3.main(Foo3.java:7)
|
||||
* Suppressed: Resource$CloseFailException: Resource ID = 2
|
||||
* at Resource.close(Resource.java:26)
|
||||
* at Foo3.main(Foo3.java:5)
|
||||
* Suppressed: Resource$CloseFailException: Resource ID = 1
|
||||
* at Resource.close(Resource.java:26)
|
||||
* at Foo3.main(Foo3.java:5)
|
||||
* Caused by: java.lang.Exception: I did it
|
||||
* at Foo3.main(Foo3.java:8)
|
||||
* </pre>
|
||||
* Likewise, a suppressed exception can have a cause:
|
||||
* <pre>
|
||||
* Exception in thread "main" java.lang.Exception: Main block
|
||||
* at Foo4.main(Foo4.java:6)
|
||||
* Suppressed: Resource2$CloseFailException: Resource ID = 1
|
||||
* at Resource2.close(Resource2.java:20)
|
||||
* at Foo4.main(Foo4.java:5)
|
||||
* Caused by: java.lang.Exception: Rats, you caught me
|
||||
* at Resource2$CloseFailException.<init>(Resource2.java:45)
|
||||
* ... 2 more
|
||||
* </pre>
|
||||
*/
|
||||
@Override
|
||||
public void printStackTrace() {
|
||||
super.printStackTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints this throwable and its backtrace to the specified print stream.
|
||||
*
|
||||
* @param s {@code PrintStream} to use for output
|
||||
*/
|
||||
@Override
|
||||
public void printStackTrace(PrintStream s) {
|
||||
super.printStackTrace(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints this throwable and its backtrace to the specified
|
||||
* print writer.
|
||||
*
|
||||
* @param s {@code PrintWriter} to use for output
|
||||
* @since JDK1.1
|
||||
*/
|
||||
@Override
|
||||
public void printStackTrace(PrintWriter s) {
|
||||
super.printStackTrace(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in the execution stack trace. This method records within this
|
||||
* {@code Throwable} object information about the current state of
|
||||
* the stack frames for the current thread.
|
||||
*
|
||||
* <p>If the stack trace of this {@code Throwable} {@linkplain
|
||||
* Throwable#Throwable(String, Throwable, boolean, boolean) is not
|
||||
* writable}, calling this method has no effect.
|
||||
*
|
||||
* @return a reference to this {@code Throwable} instance.
|
||||
* @see Throwable#printStackTrace()
|
||||
*/
|
||||
@Override
|
||||
public synchronized Throwable fillInStackTrace() {
|
||||
return super.fillInStackTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides programmatic access to the stack trace information printed by
|
||||
* {@link #printStackTrace()}. Returns an array of stack trace elements,
|
||||
* each representing one stack frame. The zeroth element of the array
|
||||
* (assuming the array's length is non-zero) represents the top of the
|
||||
* stack, which is the last method invocation in the sequence. Typically,
|
||||
* this is the point at which this throwable was created and thrown.
|
||||
* The last element of the array (assuming the array's length is non-zero)
|
||||
* represents the bottom of the stack, which is the first method invocation
|
||||
* in the sequence.
|
||||
*
|
||||
* <p>Some virtual machines may, under some circumstances, omit one
|
||||
* or more stack frames from the stack trace. In the extreme case,
|
||||
* a virtual machine that has no stack trace information concerning
|
||||
* this throwable is permitted to return a zero-length array from this
|
||||
* method. Generally speaking, the array returned by this method will
|
||||
* contain one element for every frame that would be printed by
|
||||
* {@code printStackTrace}. Writes to the returned array do not
|
||||
* affect future calls to this method.
|
||||
*
|
||||
* @return an array of stack trace elements representing the stack trace
|
||||
* pertaining to this throwable.
|
||||
* @since 1.4
|
||||
*/
|
||||
@Override
|
||||
public StackTraceElement[] getStackTrace() {
|
||||
return super.getStackTrace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stack trace elements that will be returned by
|
||||
* {@link #getStackTrace()} and printed by {@link #printStackTrace()}
|
||||
* and related methods.
|
||||
* <p>
|
||||
* This method, which is designed for use by RPC frameworks and other
|
||||
* advanced systems, allows the client to override the default
|
||||
* stack trace that is either generated by {@link #fillInStackTrace()}
|
||||
* when a throwable is constructed or deserialized when a throwable is
|
||||
* read from a serialization stream.
|
||||
*
|
||||
* <p>If the stack trace of this {@code Throwable} {@linkplain
|
||||
* Throwable#Throwable(String, Throwable, boolean, boolean) is not
|
||||
* writable}, calling this method has no effect other than
|
||||
* validating its argument.
|
||||
*
|
||||
* @param stackTrace the stack trace elements to be associated with
|
||||
* this {@code Throwable}. The specified array is copied by this
|
||||
* call; changes in the specified array after the method invocation
|
||||
* returns will have no affect on this {@code Throwable}'s stack
|
||||
* trace.
|
||||
* @throws NullPointerException if {@code stackTrace} is
|
||||
* {@code null} or if any of the elements of
|
||||
* {@code stackTrace} are {@code null}
|
||||
* @since 1.4
|
||||
*/
|
||||
@Override
|
||||
public void setStackTrace(StackTraceElement[] stackTrace) {
|
||||
super.setStackTrace(stackTrace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code value for the object. This method is
|
||||
* supported for the benefit of hash tables such as those provided by
|
||||
* {@link HashMap}.
|
||||
* <p>
|
||||
* The general contract of {@code hashCode} is:
|
||||
* <ul>
|
||||
* <li>Whenever it is invoked on the same object more than once during
|
||||
* an execution of a Java application, the {@code hashCode} method
|
||||
* must consistently return the same integer, provided no information
|
||||
* used in {@code equals} comparisons on the object is modified.
|
||||
* This integer need not remain consistent from one execution of an
|
||||
* application to another execution of the same application.
|
||||
* <li>If two objects are equal according to the {@code equals(Object)}
|
||||
* method, then calling the {@code hashCode} method on each of
|
||||
* the two objects must produce the same integer result.
|
||||
* <li>It is <em>not</em> required that if two objects are unequal
|
||||
* according to the {@link Object#equals(Object)}
|
||||
* method, then calling the {@code hashCode} method on each of the
|
||||
* two objects must produce distinct integer results. However, the
|
||||
* programmer should be aware that producing distinct integer results
|
||||
* for unequal objects may improve the performance of hash tables.
|
||||
* </ul>
|
||||
* <p>
|
||||
* As much as is reasonably practical, the hashCode method defined by
|
||||
* class {@code Object} does return distinct integers for distinct
|
||||
* objects. (This is typically implemented by converting the internal
|
||||
* address of the object into an integer, but this implementation
|
||||
* technique is not required by the
|
||||
* Java™ programming language.)
|
||||
*
|
||||
* @return a hash code value for this object.
|
||||
* @see Object#equals(Object)
|
||||
* @see System#identityHashCode
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether some other object is "equal to" this one.
|
||||
* <p>
|
||||
* The {@code equals} method implements an equivalence relation
|
||||
* on non-null object references:
|
||||
* <ul>
|
||||
* <li>It is <i>reflexive</i>: for any non-null reference value
|
||||
* {@code x}, {@code x.equals(x)} should return
|
||||
* {@code true}.
|
||||
* <li>It is <i>symmetric</i>: for any non-null reference values
|
||||
* {@code x} and {@code y}, {@code x.equals(y)}
|
||||
* should return {@code true} if and only if
|
||||
* {@code y.equals(x)} returns {@code true}.
|
||||
* <li>It is <i>transitive</i>: for any non-null reference values
|
||||
* {@code x}, {@code y}, and {@code z}, if
|
||||
* {@code x.equals(y)} returns {@code true} and
|
||||
* {@code y.equals(z)} returns {@code true}, then
|
||||
* {@code x.equals(z)} should return {@code true}.
|
||||
* <li>It is <i>consistent</i>: for any non-null reference values
|
||||
* {@code x} and {@code y}, multiple invocations of
|
||||
* {@code x.equals(y)} consistently return {@code true}
|
||||
* or consistently return {@code false}, provided no
|
||||
* information used in {@code equals} comparisons on the
|
||||
* objects is modified.
|
||||
* <li>For any non-null reference value {@code x},
|
||||
* {@code x.equals(null)} should return {@code false}.
|
||||
* </ul>
|
||||
* <p>
|
||||
* The {@code equals} method for class {@code Object} implements
|
||||
* the most discriminating possible equivalence relation on objects;
|
||||
* that is, for any non-null reference values {@code x} and
|
||||
* {@code y}, this method returns {@code true} if and only
|
||||
* if {@code x} and {@code y} refer to the same object
|
||||
* ({@code x == y} has the value {@code true}).
|
||||
* <p>
|
||||
* Note that it is generally necessary to override the {@code hashCode}
|
||||
* method whenever this method is overridden, so as to maintain the
|
||||
* general contract for the {@code hashCode} method, which states
|
||||
* that equal objects must have equal hash codes.
|
||||
*
|
||||
* @param obj the reference object with which to compare.
|
||||
* @return {@code true} if this object is the same as the obj
|
||||
* argument; {@code false} otherwise.
|
||||
* @see #hashCode()
|
||||
* @see HashMap
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a copy of this object. The precise meaning
|
||||
* of "copy" may depend on the class of the object. The general
|
||||
* intent is that, for any object {@code x}, the expression:
|
||||
* <blockquote>
|
||||
* <pre>
|
||||
* x.clone() != x</pre></blockquote>
|
||||
* will be true, and that the expression:
|
||||
* <blockquote>
|
||||
* <pre>
|
||||
* x.clone().getClass() == x.getClass()</pre></blockquote>
|
||||
* will be {@code true}, but these are not absolute requirements.
|
||||
* While it is typically the case that:
|
||||
* <blockquote>
|
||||
* <pre>
|
||||
* x.clone().equals(x)</pre></blockquote>
|
||||
* will be {@code true}, this is not an absolute requirement.
|
||||
* <p>
|
||||
* By convention, the returned object should be obtained by calling
|
||||
* {@code super.clone}. If a class and all of its superclasses (except
|
||||
* {@code Object}) obey this convention, it will be the case that
|
||||
* {@code x.clone().getClass() == x.getClass()}.
|
||||
* <p>
|
||||
* By convention, the object returned by this method should be independent
|
||||
* of this object (which is being cloned). To achieve this independence,
|
||||
* it may be necessary to modify one or more fields of the object returned
|
||||
* by {@code super.clone} before returning it. Typically, this means
|
||||
* copying any mutable objects that comprise the internal "deep structure"
|
||||
* of the object being cloned and replacing the references to these
|
||||
* objects with references to the copies. If a class contains only
|
||||
* primitive fields or references to immutable objects, then it is usually
|
||||
* the case that no fields in the object returned by {@code super.clone}
|
||||
* need to be modified.
|
||||
* <p>
|
||||
* The method {@code clone} for class {@code Object} performs a
|
||||
* specific cloning operation. First, if the class of this object does
|
||||
* not implement the interface {@code Cloneable}, then a
|
||||
* {@code CloneNotSupportedException} is thrown. Note that all arrays
|
||||
* are considered to implement the interface {@code Cloneable} and that
|
||||
* the return type of the {@code clone} method of an array type {@code T[]}
|
||||
* is {@code T[]} where T is any reference or primitive type.
|
||||
* Otherwise, this method creates a new instance of the class of this
|
||||
* object and initializes all its fields with exactly the contents of
|
||||
* the corresponding fields of this object, as if by assignment; the
|
||||
* contents of the fields are not themselves cloned. Thus, this method
|
||||
* performs a "shallow copy" of this object, not a "deep copy" operation.
|
||||
* <p>
|
||||
* The class {@code Object} does not itself implement the interface
|
||||
* {@code Cloneable}, so calling the {@code clone} method on an object
|
||||
* whose class is {@code Object} will result in throwing an
|
||||
* exception at run time.
|
||||
*
|
||||
* @return a clone of this instance.
|
||||
* @throws CloneNotSupportedException if the object's class does not
|
||||
* support the {@code Cloneable} interface. Subclasses
|
||||
* that override the {@code clone} method can also
|
||||
* throw this exception to indicate that an instance cannot
|
||||
* be cloned.
|
||||
* @see Cloneable
|
||||
*/
|
||||
@Override
|
||||
protected Object clone() throws CloneNotSupportedException {
|
||||
return super.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the garbage collector on an object when garbage collection
|
||||
* determines that there are no more references to the object.
|
||||
* A subclass overrides the {@code finalize} method to dispose of
|
||||
* system resources or to perform other cleanup.
|
||||
* <p>
|
||||
* The general contract of {@code finalize} is that it is invoked
|
||||
* if and when the Java™ virtual
|
||||
* machine has determined that there is no longer any
|
||||
* means by which this object can be accessed by any thread that has
|
||||
* not yet died, except as a result of an action taken by the
|
||||
* finalization of some other object or class which is ready to be
|
||||
* finalized. The {@code finalize} method may take any action, including
|
||||
* making this object available again to other threads; the usual purpose
|
||||
* of {@code finalize}, however, is to perform cleanup actions before
|
||||
* the object is irrevocably discarded. For example, the finalize method
|
||||
* for an object that represents an input/output connection might perform
|
||||
* explicit I/O transactions to break the connection before the object is
|
||||
* permanently discarded.
|
||||
* <p>
|
||||
* The {@code finalize} method of class {@code Object} performs no
|
||||
* special action; it simply returns normally. Subclasses of
|
||||
* {@code Object} may override this definition.
|
||||
* <p>
|
||||
* The Java programming language does not guarantee which thread will
|
||||
* invoke the {@code finalize} method for any given object. It is
|
||||
* guaranteed, however, that the thread that invokes finalize will not
|
||||
* be holding any user-visible synchronization locks when finalize is
|
||||
* invoked. If an uncaught exception is thrown by the finalize method,
|
||||
* the exception is ignored and finalization of that object terminates.
|
||||
* <p>
|
||||
* After the {@code finalize} method has been invoked for an object, no
|
||||
* further action is taken until the Java virtual machine has again
|
||||
* determined that there is no longer any means by which this object can
|
||||
* be accessed by any thread that has not yet died, including possible
|
||||
* actions by other objects or classes which are ready to be finalized,
|
||||
* at which point the object may be discarded.
|
||||
* <p>
|
||||
* The {@code finalize} method is never invoked more than once by a Java
|
||||
* virtual machine for any given object.
|
||||
* <p>
|
||||
* Any exception thrown by the {@code finalize} method causes
|
||||
* the finalization of this object to be halted, but is otherwise
|
||||
* ignored.
|
||||
*
|
||||
* @throws Throwable the {@code Exception} raised by this method
|
||||
* @jls 12.6 Finalization of Class Instances
|
||||
* @see WeakReference
|
||||
* @see PhantomReference
|
||||
*/
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
public enum ChainStateField {
|
||||
INSTANCE_ID,
|
||||
STATUS,
|
||||
MESSAGE,
|
||||
ERROR,
|
||||
MEMORY,
|
||||
PAYLOAD,
|
||||
NODE_STATES,
|
||||
COMPUTE_COST,
|
||||
SUSPEND_NODE_IDS,
|
||||
SUSPEND_FOR_PARAMETERS,
|
||||
EXECUTE_RESULT,
|
||||
CHAIN_DEFINITION_ID,
|
||||
ENVIRONMENT,
|
||||
CHILD_STATE_IDS,
|
||||
PARENT_INSTANCE_ID,
|
||||
TRIGGER_NODE_IDS,
|
||||
TRIGGER_EDGE_IDS,
|
||||
UNCHECKED_EDGE_IDS,
|
||||
UNCHECKED_NODE_IDS;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public interface ChainStateModifier {
|
||||
|
||||
EnumSet<ChainStateField> modify(ChainState state);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public interface ChainStateRepository {
|
||||
|
||||
ChainState load(String instanceId);
|
||||
|
||||
boolean tryUpdate(ChainState newState, EnumSet<ChainStateField> fields);
|
||||
|
||||
/**
|
||||
* 获取指定 instanceId 的分布式锁
|
||||
*
|
||||
* @param instanceId 链实例 ID
|
||||
* @param timeout 获取锁的超时时间
|
||||
* @param unit 时间单位
|
||||
* @return ChainLock 句柄,调用方必须负责 close()
|
||||
* @throws IllegalArgumentException if instanceId is blank
|
||||
*/
|
||||
default ChainLock getLock(String instanceId, long timeout, TimeUnit unit) {
|
||||
return new LocalChainLock(instanceId, timeout, unit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.util.MapUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class InMemoryChainStateRepository implements ChainStateRepository {
|
||||
private static final Map<String, ChainState> chainStateMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public ChainState load(String instanceId) {
|
||||
return MapUtil.computeIfAbsent(chainStateMap, instanceId, k -> {
|
||||
ChainState state = new ChainState();
|
||||
state.setInstanceId(instanceId);
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryUpdate(ChainState chainState, EnumSet<ChainStateField> fields) {
|
||||
chainStateMap.put(chainState.getInstanceId(), chainState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
import com.easyagents.flow.core.util.MapUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class InMemoryNodeStateRepository implements NodeStateRepository {
|
||||
|
||||
private static final Map<String, NodeState> chainStateMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public NodeState load(String instanceId, String nodeId) {
|
||||
String key = instanceId + "." + nodeId;
|
||||
return MapUtil.computeIfAbsent(chainStateMap, key, k -> {
|
||||
NodeState nodeState = new NodeState();
|
||||
nodeState.setChainInstanceId(instanceId);
|
||||
nodeState.setNodeId(nodeId);
|
||||
return nodeState;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryUpdate(NodeState newState, EnumSet<NodeStateField> fields, long version) {
|
||||
chainStateMap.put(newState.getChainInstanceId() + "." + newState.getNodeId(), newState);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class LocalChainLock implements ChainLock {
|
||||
|
||||
private static final Map<String, LockRef> GLOBAL_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
private final String instanceId;
|
||||
private final ReentrantLock lock;
|
||||
private final boolean acquired;
|
||||
|
||||
public LocalChainLock(String instanceId, long timeout, TimeUnit unit) {
|
||||
if (instanceId == null || instanceId.isEmpty()) {
|
||||
throw new IllegalArgumentException("instanceId must not be blank");
|
||||
}
|
||||
this.instanceId = instanceId;
|
||||
|
||||
// 获取或创建锁(带引用计数)
|
||||
LockRef lockRef = GLOBAL_LOCKS.compute(instanceId, (key, ref) -> {
|
||||
if (ref == null) {
|
||||
return new LockRef(new ReentrantLock());
|
||||
} else {
|
||||
ref.refCount.incrementAndGet();
|
||||
return ref;
|
||||
}
|
||||
});
|
||||
|
||||
this.lock = lockRef.lock;
|
||||
boolean locked = false;
|
||||
try {
|
||||
if (timeout <= 0) {
|
||||
lock.lock();
|
||||
locked = true;
|
||||
} else {
|
||||
locked = lock.tryLock(timeout, unit);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
this.acquired = locked;
|
||||
|
||||
// 如果获取失败,清理引用计数
|
||||
if (!locked) {
|
||||
releaseRef();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAcquired() {
|
||||
return acquired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (acquired) {
|
||||
lock.unlock();
|
||||
releaseRef();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseRef() {
|
||||
GLOBAL_LOCKS.computeIfPresent(instanceId, (key, ref) -> {
|
||||
if (ref.refCount.decrementAndGet() <= 0) {
|
||||
return null; // 移除,允许 GC
|
||||
}
|
||||
return ref;
|
||||
});
|
||||
}
|
||||
|
||||
// 内部类:带引用计数的锁包装
|
||||
private static class LockRef {
|
||||
final ReentrantLock lock;
|
||||
final java.util.concurrent.atomic.AtomicInteger refCount = new java.util.concurrent.atomic.AtomicInteger(1);
|
||||
|
||||
LockRef(ReentrantLock lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
public enum NodeStateField {
|
||||
INSTANCE_ID,
|
||||
STATUS,
|
||||
MESSAGE,
|
||||
ERROR,
|
||||
MEMORY,
|
||||
PAYLOAD,
|
||||
NODE_STATES,
|
||||
COMPUTE_COST,
|
||||
SUSPEND_NODE_IDS,
|
||||
SUSPEND_FOR_PARAMETERS,
|
||||
EXECUTE_RESULT,
|
||||
RETRY_COUNT, EXECUTE_COUNT, EXECUTE_EDGE_IDS, LOOP_COUNT, TRIGGER_COUNT, TRIGGER_EDGE_IDS, ENVIRONMENT
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public interface NodeStateModifier {
|
||||
|
||||
EnumSet<NodeStateField> modify(NodeState state);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.repository;
|
||||
|
||||
import com.easyagents.flow.core.chain.NodeState;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
public interface NodeStateRepository {
|
||||
|
||||
NodeState load(String instanceId, String nodeId);
|
||||
|
||||
boolean tryUpdate(NodeState newState, EnumSet<NodeStateField> fields, long chainStateVersion);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.chain.event.ChainStatusChangeEvent;
|
||||
import com.easyagents.flow.core.chain.listener.ChainErrorListener;
|
||||
import com.easyagents.flow.core.chain.listener.ChainEventListener;
|
||||
import com.easyagents.flow.core.chain.listener.ChainOutputListener;
|
||||
import com.easyagents.flow.core.chain.listener.NodeErrorListener;
|
||||
import com.easyagents.flow.core.chain.repository.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* TinyFlow 最新 ChainExecutor
|
||||
* <p>
|
||||
* 说明:
|
||||
* * 负责触发 Chain 执行 / 恢复
|
||||
* * 不持有长时间运行的 Chain 实例
|
||||
* * 支持 async-only 架构
|
||||
*/
|
||||
public class ChainExecutor {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ChainExecutor.class);
|
||||
private final ChainDefinitionRepository definitionRepository;
|
||||
private final ChainStateRepository chainStateRepository;
|
||||
private final NodeStateRepository nodeStateRepository;
|
||||
private final TriggerScheduler triggerScheduler;
|
||||
private final EventManager eventManager = new EventManager();
|
||||
|
||||
public ChainExecutor(ChainDefinitionRepository definitionRepository
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
) {
|
||||
this.definitionRepository = definitionRepository;
|
||||
this.chainStateRepository = chainStateRepository;
|
||||
this.nodeStateRepository = nodeStateRepository;
|
||||
this.triggerScheduler = ChainRuntime.triggerScheduler();
|
||||
this.triggerScheduler.registerConsumer(this::accept);
|
||||
}
|
||||
|
||||
|
||||
public ChainExecutor(ChainDefinitionRepository definitionRepository
|
||||
, ChainStateRepository chainStateRepository
|
||||
, NodeStateRepository nodeStateRepository
|
||||
, TriggerScheduler triggerScheduler) {
|
||||
this.definitionRepository = definitionRepository;
|
||||
this.chainStateRepository = chainStateRepository;
|
||||
this.nodeStateRepository = nodeStateRepository;
|
||||
this.triggerScheduler = triggerScheduler;
|
||||
this.triggerScheduler.registerConsumer(this::accept);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> execute(String definitionId, Map<String, Object> variables) {
|
||||
return execute(definitionId, variables, Long.MAX_VALUE, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> execute(String definitionId, Map<String, Object> variables, long timeout, TimeUnit unit) {
|
||||
Chain chain = createChain(definitionId);
|
||||
String stateInstanceId = chain.getStateInstanceId();
|
||||
CompletableFuture<Map<String, Object>> future = new CompletableFuture<>();
|
||||
|
||||
ChainEventListener listener = (event, c) -> {
|
||||
if (event instanceof ChainStatusChangeEvent) {
|
||||
if (((ChainStatusChangeEvent) event).getStatus().isTerminal()
|
||||
&& c.getStateInstanceId().equals(stateInstanceId)) {
|
||||
ChainState state = chainStateRepository.load(stateInstanceId);
|
||||
Map<String, Object> execResult = state.getExecuteResult();
|
||||
future.complete(execResult != null ? execResult : Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ChainErrorListener errorListener = (error, c) -> {
|
||||
if (c.getStateInstanceId().equals(stateInstanceId)) {
|
||||
future.completeExceptionally(error);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
this.addEventListener(listener);
|
||||
this.addErrorListener(errorListener);
|
||||
chain.start(variables);
|
||||
Map<String, Object> result = future.get(timeout, unit);
|
||||
clearDefaultStates(result);
|
||||
return result;
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
throw new RuntimeException("Execution timed out", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.cancel(true);
|
||||
throw new RuntimeException("Execution interrupted", e);
|
||||
} catch (Throwable e) {
|
||||
future.cancel(true);
|
||||
throw new RuntimeException("Execution failed", e.getCause());
|
||||
} finally {
|
||||
this.removeEventListener(listener);
|
||||
this.removeErrorListener(errorListener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理默认状态
|
||||
*
|
||||
* @param result 执行结果
|
||||
*/
|
||||
public void clearDefaultStates(Map<String, Object> result) {
|
||||
if (result == null || result.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
result.remove(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY);
|
||||
result.remove(ChainConsts.NODE_STATE_STATUS_KEY);
|
||||
result.remove(ChainConsts.CHAIN_STATE_STATUS_KEY);
|
||||
result.remove(ChainConsts.CHAIN_STATE_MESSAGE_KEY);
|
||||
}
|
||||
|
||||
public String executeAsync(String definitionId, Map<String, Object> variables) {
|
||||
Chain chain = createChain(definitionId);
|
||||
chain.start(variables);
|
||||
return chain.getStateInstanceId();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 执行指定节点的业务逻辑
|
||||
*
|
||||
* @param definitionId 流程定义ID,用于标识哪个流程定义
|
||||
* @param nodeId 节点ID,用于标识要执行的具体节点
|
||||
* @param variables 执行上下文变量集合,包含节点执行所需的参数和数据
|
||||
* @return 执行结果映射表,包含节点执行后的输出数据
|
||||
*/
|
||||
public Map<String, Object> executeNode(String definitionId, String nodeId, Map<String, Object> variables) {
|
||||
ChainDefinition chainDefinitionById = definitionRepository.getChainDefinitionById(definitionId);
|
||||
Node node = chainDefinitionById.getNodeById(nodeId);
|
||||
Chain temp = createChain(definitionId);
|
||||
if (variables != null && !variables.isEmpty()) {
|
||||
temp.updateStateSafely(s -> {
|
||||
s.getMemory().putAll(variables);
|
||||
return EnumSet.of(ChainStateField.MEMORY);
|
||||
});
|
||||
}
|
||||
return node.execute(temp);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定节点的参数列表
|
||||
*
|
||||
* @param definitionId 链定义ID,用于定位具体的链定义
|
||||
* @param nodeId 节点ID,用于在链定义中定位具体节点
|
||||
* @return 返回指定节点的参数列表
|
||||
*/
|
||||
public List<Parameter> getNodeParameters(String definitionId, String nodeId) {
|
||||
ChainDefinition chainDefinitionById = definitionRepository.getChainDefinitionById(definitionId);
|
||||
Node node = chainDefinitionById.getNodeById(nodeId);
|
||||
return node.getParameters();
|
||||
}
|
||||
|
||||
|
||||
public void resumeAsync(String stateInstanceId) {
|
||||
this.resumeAsync(stateInstanceId, Collections.emptyMap());
|
||||
}
|
||||
|
||||
|
||||
public void resumeAsync(String stateInstanceId, Map<String, Object> variables) {
|
||||
ChainState state = chainStateRepository.load(stateInstanceId);
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChainDefinition definition = definitionRepository.getChainDefinitionById(state.getChainDefinitionId());
|
||||
if (definition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Chain chain = new Chain(definition, state.getInstanceId());
|
||||
chain.setTriggerScheduler(triggerScheduler);
|
||||
chain.setChainStateRepository(chainStateRepository);
|
||||
chain.setNodeStateRepository(nodeStateRepository);
|
||||
chain.setEventManager(eventManager);
|
||||
|
||||
chain.resume(variables);
|
||||
}
|
||||
|
||||
|
||||
private Chain createChain(String definitionId) {
|
||||
ChainDefinition definition = definitionRepository.getChainDefinitionById(definitionId);
|
||||
if (definition == null) {
|
||||
throw new RuntimeException("Chain definition not found");
|
||||
}
|
||||
|
||||
String stateInstanceId = UUID.randomUUID().toString();
|
||||
Chain chain = new Chain(definition, stateInstanceId);
|
||||
chain.setTriggerScheduler(triggerScheduler);
|
||||
chain.setChainStateRepository(chainStateRepository);
|
||||
chain.setNodeStateRepository(nodeStateRepository);
|
||||
chain.setEventManager(eventManager);
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
|
||||
private void accept(Trigger trigger, ExecutorService worker) {
|
||||
ChainState state = chainStateRepository.load(trigger.getStateInstanceId());
|
||||
if (state == null) {
|
||||
throw new ChainException("Chain state not found");
|
||||
}
|
||||
|
||||
|
||||
ChainDefinition definition = definitionRepository.getChainDefinitionById(state.getChainDefinitionId());
|
||||
if (definition == null) {
|
||||
throw new ChainException("Chain definition not found");
|
||||
}
|
||||
|
||||
Chain chain = new Chain(definition, trigger.getStateInstanceId());
|
||||
chain.setTriggerScheduler(triggerScheduler);
|
||||
chain.setChainStateRepository(chainStateRepository);
|
||||
chain.setNodeStateRepository(nodeStateRepository);
|
||||
chain.setEventManager(eventManager);
|
||||
|
||||
String nodeId = trigger.getNodeId();
|
||||
if (nodeId == null) {
|
||||
throw new ChainException("Node ID not found in trigger.");
|
||||
}
|
||||
|
||||
Node node = definition.getNodeById(nodeId);
|
||||
if (node == null) {
|
||||
throw new ChainException("Node not found in definition(id: " + definition.getId() + ")");
|
||||
}
|
||||
|
||||
chain.executeNode(node, trigger);
|
||||
}
|
||||
|
||||
|
||||
public synchronized void addEventListener(Class<? extends Event> eventClass, ChainEventListener listener) {
|
||||
eventManager.addEventListener(eventClass, listener);
|
||||
}
|
||||
|
||||
public synchronized void addEventListener(ChainEventListener listener) {
|
||||
eventManager.addEventListener(listener);
|
||||
}
|
||||
|
||||
public synchronized void removeEventListener(ChainEventListener listener) {
|
||||
eventManager.removeEventListener(listener);
|
||||
}
|
||||
|
||||
public synchronized void removeEventListener(Class<? extends Event> eventClass, ChainEventListener listener) {
|
||||
eventManager.removeEventListener(eventClass, listener);
|
||||
}
|
||||
|
||||
public synchronized void addErrorListener(ChainErrorListener listener) {
|
||||
eventManager.addChainErrorListener(listener);
|
||||
}
|
||||
|
||||
public synchronized void removeErrorListener(ChainErrorListener listener) {
|
||||
eventManager.removeChainErrorListener(listener);
|
||||
}
|
||||
|
||||
public synchronized void addNodeErrorListener(NodeErrorListener listener) {
|
||||
eventManager.addNodeErrorListener(listener);
|
||||
}
|
||||
|
||||
public synchronized void removeNodeErrorListener(NodeErrorListener listener) {
|
||||
eventManager.removeNodeErrorListener(listener);
|
||||
}
|
||||
|
||||
public void addOutputListener(ChainOutputListener outputListener) {
|
||||
eventManager.addOutputListener(outputListener);
|
||||
}
|
||||
|
||||
public ChainDefinitionRepository getDefinitionRepository() {
|
||||
return definitionRepository;
|
||||
}
|
||||
|
||||
public ChainStateRepository getChainStateRepository() {
|
||||
return chainStateRepository;
|
||||
}
|
||||
|
||||
public NodeStateRepository getNodeStateRepository() {
|
||||
return nodeStateRepository;
|
||||
}
|
||||
|
||||
public TriggerScheduler getTriggerScheduler() {
|
||||
return triggerScheduler;
|
||||
}
|
||||
|
||||
public EventManager getEventManager() {
|
||||
return eventManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class ChainRuntime {
|
||||
|
||||
private static final int NODE_POOL_CORE = 32;
|
||||
private static final int NODE_POOL_MAX = 512;
|
||||
private static final int CHAIN_POOL_CORE = 8;
|
||||
private static final int CHAIN_POOL_MAX = 64;
|
||||
|
||||
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> {
|
||||
Thread t = new Thread(r, "tinyflow-trigger-scheduler");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private static final ExecutorService ASYNC_NODE_EXECUTORS =
|
||||
new ThreadPoolExecutor(
|
||||
NODE_POOL_CORE, NODE_POOL_MAX,
|
||||
60L, TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(10000),
|
||||
r -> new Thread(r, "tinyflow-node-exec"));
|
||||
|
||||
|
||||
private static final TriggerScheduler TRIGGER_SCHEDULER = new TriggerScheduler(
|
||||
new InMemoryTriggerStore()
|
||||
, scheduler
|
||||
, ASYNC_NODE_EXECUTORS
|
||||
, 10000L);
|
||||
|
||||
|
||||
public static TriggerScheduler triggerScheduler() {
|
||||
return TRIGGER_SCHEDULER;
|
||||
}
|
||||
|
||||
static {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
TRIGGER_SCHEDULER.shutdown();
|
||||
ASYNC_NODE_EXECUTORS.shutdownNow();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class InMemoryTriggerStore implements TriggerStore {
|
||||
|
||||
private final ConcurrentHashMap<String, Trigger> store = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Trigger save(Trigger trigger) {
|
||||
if (trigger.getId() == null) {
|
||||
trigger.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
store.put(trigger.getId(), trigger);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(String triggerId) {
|
||||
return store.remove(triggerId) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Trigger find(String triggerId) {
|
||||
return store.get(triggerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Trigger> findDue(long uptoTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Trigger> findAllPending() {
|
||||
return new ArrayList<>(store.values());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Trigger implements Serializable {
|
||||
private String id;
|
||||
private String stateInstanceId;
|
||||
private String edgeId;
|
||||
private String nodeId; // 可以为 null,代表触发整个 chain
|
||||
private TriggerType type;
|
||||
private long triggerAt; // epoch ms
|
||||
|
||||
public Trigger() {
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
public String getStateInstanceId() {
|
||||
return stateInstanceId;
|
||||
}
|
||||
|
||||
public void setStateInstanceId(String stateInstanceId) {
|
||||
this.stateInstanceId = stateInstanceId;
|
||||
}
|
||||
|
||||
public String getEdgeId() {
|
||||
return edgeId;
|
||||
}
|
||||
|
||||
public void setEdgeId(String edgeId) {
|
||||
this.edgeId = edgeId;
|
||||
}
|
||||
|
||||
public String getNodeId() {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
public void setNodeId(String nodeId) {
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public TriggerType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(TriggerType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public long getTriggerAt() {
|
||||
return triggerAt;
|
||||
}
|
||||
|
||||
public void setTriggerAt(long triggerAt) {
|
||||
this.triggerAt = triggerAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Trigger{" +
|
||||
"id='" + id + '\'' +
|
||||
", stateInstanceId='" + stateInstanceId + '\'' +
|
||||
", edgeId='" + edgeId + '\'' +
|
||||
", nodeId='" + nodeId + '\'' +
|
||||
", type=" + type +
|
||||
", triggerAt=" + triggerAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
public class TriggerContext {
|
||||
private static final ThreadLocal<Trigger> currentTrigger = new ThreadLocal<>();
|
||||
public static Trigger getCurrentTrigger() {
|
||||
return currentTrigger.get();
|
||||
}
|
||||
public static void setCurrentTrigger(Trigger trigger) {
|
||||
currentTrigger.set(trigger);
|
||||
}
|
||||
public static void clearCurrentTrigger() {
|
||||
currentTrigger.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
*
|
||||
* 功能:
|
||||
* - schedule trigger (持久化到 TriggerStore 并 schedule)
|
||||
* - cancel trigger
|
||||
* - fire(triggerId) 用于 webhook/event/manual 主动触发
|
||||
* - recoverAndSchedulePending() 启动时恢复未执行的 trigger
|
||||
* - periodical scan findDue(upto) 以保证重启/宕机后补偿触发
|
||||
* <p>
|
||||
* 注意: 分布式环境下需要在 TriggerStore 层提供抢占/锁逻辑(例如 lease/owner 字段)。
|
||||
*/
|
||||
public class TriggerScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TriggerScheduler.class);
|
||||
private final TriggerStore store;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final ExecutorService worker;
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
|
||||
// map 用于管理取消:triggerId -> ScheduledFuture
|
||||
private final ConcurrentMap<String, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
|
||||
|
||||
// consumer 来把 trigger 交给 ChainExecutor(或 ChainRuntime)去处理
|
||||
private volatile TriggerConsumer consumer;
|
||||
|
||||
// 周期扫查间隔(ms)
|
||||
private final long scanIntervalMs;
|
||||
|
||||
// 扫描任务 future
|
||||
private ScheduledFuture<?> scanFuture;
|
||||
|
||||
public interface TriggerConsumer {
|
||||
void accept(Trigger trigger, ExecutorService worker);
|
||||
}
|
||||
|
||||
public TriggerScheduler(TriggerStore store, ScheduledExecutorService scheduler, ExecutorService worker, long scanIntervalMs) {
|
||||
this.store = Objects.requireNonNull(store, "TriggerStore required");
|
||||
this.scheduler = Objects.requireNonNull(scheduler, "ScheduledExecutorService required");
|
||||
this.worker = Objects.requireNonNull(worker, "ExecutorService required");
|
||||
this.scanIntervalMs = Math.max(1000, scanIntervalMs);
|
||||
|
||||
// 恢复并 schedule
|
||||
recoverAndSchedulePending();
|
||||
|
||||
// 启动周期扫查 findDue
|
||||
startPeriodicScan();
|
||||
}
|
||||
|
||||
|
||||
public void registerConsumer(TriggerConsumer consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* schedule a trigger: persist -> schedule (单机语义)
|
||||
*/
|
||||
public Trigger schedule(Trigger trigger) {
|
||||
if (closed.get()) throw new IllegalStateException("TriggerScheduler closed");
|
||||
if (trigger.getId() == null) {
|
||||
trigger.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
store.save(trigger);
|
||||
scheduleInternal(trigger);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* cancel trigger (从 store 删除并尝试取消已 schedule 的 future)
|
||||
*/
|
||||
public boolean cancel(String triggerId) {
|
||||
boolean removed = store.remove(triggerId);
|
||||
ScheduledFuture<?> f = scheduledFutures.remove(triggerId);
|
||||
if (f != null) {
|
||||
f.cancel(false);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动触发(webhook/event/manual 场景)
|
||||
*/
|
||||
public boolean fire(String triggerId) {
|
||||
if (closed.get()) return false;
|
||||
Trigger t = store.find(triggerId);
|
||||
if (t == null) return false;
|
||||
if (consumer == null) {
|
||||
// 无 consumer,仍从 store 中移除
|
||||
store.remove(triggerId);
|
||||
return false;
|
||||
}
|
||||
// 在 worker 线程触发 consumer
|
||||
worker.submit(() -> {
|
||||
try {
|
||||
consumer.accept(t, worker);
|
||||
} catch (Exception e) {
|
||||
log.error(e.toString(), e);
|
||||
} finally {
|
||||
// 默认语义:触发后移除
|
||||
store.remove(triggerId);
|
||||
ScheduledFuture<?> sf = scheduledFutures.remove(triggerId);
|
||||
if (sf != null) sf.cancel(false);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* internal scheduling for a trigger (单机 scheduled semantics)
|
||||
*/
|
||||
private void scheduleInternal(Trigger trigger) {
|
||||
if (closed.get()) return;
|
||||
|
||||
long delay = Math.max(0, trigger.getTriggerAt() - System.currentTimeMillis());
|
||||
|
||||
// cancel any existing scheduled future for same id
|
||||
ScheduledFuture<?> prev = scheduledFutures.remove(trigger.getId());
|
||||
if (prev != null) prev.cancel(false);
|
||||
|
||||
ScheduledFuture<?> future = scheduler.schedule(() -> {
|
||||
// double-check existence in store (可能已被 cancel)
|
||||
Trigger existing = store.find(trigger.getId());
|
||||
if (existing == null) {
|
||||
scheduledFutures.remove(trigger.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (consumer != null) {
|
||||
worker.submit(() -> {
|
||||
try {
|
||||
TriggerContext.setCurrentTrigger(existing);
|
||||
consumer.accept(existing, worker);
|
||||
} catch (Throwable e) {
|
||||
log.error(e.toString(), e);
|
||||
} finally {
|
||||
TriggerContext.clearCurrentTrigger();
|
||||
store.remove(existing.getId());
|
||||
scheduledFutures.remove(existing.getId());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 无 consumer,则移除
|
||||
store.remove(existing.getId());
|
||||
scheduledFutures.remove(existing.getId());
|
||||
}
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
|
||||
scheduledFutures.put(trigger.getId(), future);
|
||||
}
|
||||
|
||||
private void recoverAndSchedulePending() {
|
||||
try {
|
||||
List<Trigger> list = store.findAllPending();
|
||||
if (list == null || list.isEmpty()) return;
|
||||
for (Trigger t : list) {
|
||||
scheduleInternal(t);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// 忽略单次恢复错误,继续运行
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void startPeriodicScan() {
|
||||
if (closed.get()) return;
|
||||
scanFuture = scheduler.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
long upto = System.currentTimeMillis();
|
||||
List<Trigger> due = store.findDue(upto);
|
||||
if (due == null || due.isEmpty()) return;
|
||||
for (Trigger t : due) {
|
||||
// 如果已被 schedule 到未来(scheduledFutures 包含且尚未到期),跳过
|
||||
ScheduledFuture<?> sf = scheduledFutures.get(t.getId());
|
||||
if (sf != null && !sf.isDone() && !sf.isCancelled()) {
|
||||
continue;
|
||||
}
|
||||
// 直接提交到 worker,让 consumer 处理;并从 store 中移除
|
||||
if (consumer != null) {
|
||||
worker.submit(() -> {
|
||||
try {
|
||||
consumer.accept(t, worker);
|
||||
} finally {
|
||||
store.remove(t.getId());
|
||||
ScheduledFuture<?> f2 = scheduledFutures.remove(t.getId());
|
||||
if (f2 != null) f2.cancel(false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
store.remove(t.getId());
|
||||
}
|
||||
}
|
||||
} catch (Throwable tt) {
|
||||
tt.printStackTrace();
|
||||
}
|
||||
}, scanIntervalMs, scanIntervalMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
if (scanFuture != null) scanFuture.cancel(false);
|
||||
// cancel scheduled futures
|
||||
for (Map.Entry<String, ScheduledFuture<?>> e : scheduledFutures.entrySet()) {
|
||||
try {
|
||||
e.getValue().cancel(false);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
scheduledFutures.clear();
|
||||
|
||||
try {
|
||||
scheduler.shutdownNow();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
worker.shutdownNow();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TriggerStore {
|
||||
Trigger save(Trigger trigger);
|
||||
|
||||
boolean remove(String triggerId);
|
||||
|
||||
Trigger find(String triggerId);
|
||||
|
||||
List<Trigger> findDue(long uptoTimestamp);
|
||||
|
||||
List<Trigger> findAllPending();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.chain.runtime;
|
||||
|
||||
public enum TriggerType {
|
||||
START,
|
||||
NEXT,
|
||||
PARENT,
|
||||
CHILD,
|
||||
SELF,
|
||||
LOOP,
|
||||
RETRY,
|
||||
TIMER,
|
||||
CRON,
|
||||
EVENT,
|
||||
DELAY,
|
||||
RESUME
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.code;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface CodeRuntimeEngine {
|
||||
Map<String, Object> execute(String code, CodeNode node, Chain chain);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.code;
|
||||
|
||||
import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CodeRuntimeEngineManager {
|
||||
|
||||
public List<CodeRuntimeEngineProvider> providers = new ArrayList<>();
|
||||
|
||||
private static class ManagerHolder {
|
||||
private static final CodeRuntimeEngineManager INSTANCE = new CodeRuntimeEngineManager();
|
||||
}
|
||||
|
||||
private CodeRuntimeEngineManager() {
|
||||
JavascriptRuntimeEngine javascriptRuntimeEngine = new JavascriptRuntimeEngine();
|
||||
providers.add(engineId -> {
|
||||
if ("js".equals(engineId) || "javascript".equals(engineId)) {
|
||||
return javascriptRuntimeEngine;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static CodeRuntimeEngineManager getInstance() {
|
||||
return ManagerHolder.INSTANCE;
|
||||
}
|
||||
|
||||
public void registerProvider(CodeRuntimeEngineProvider provider) {
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
public void removeProvider(CodeRuntimeEngineProvider provider) {
|
||||
providers.remove(provider);
|
||||
}
|
||||
|
||||
public CodeRuntimeEngine getCodeRuntimeEngine(Object engineId) {
|
||||
for (CodeRuntimeEngineProvider provider : providers) {
|
||||
CodeRuntimeEngine codeRuntimeEngine = provider.getCodeRuntimeEngine(engineId);
|
||||
if (codeRuntimeEngine != null) {
|
||||
return codeRuntimeEngine;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.code;
|
||||
|
||||
public interface CodeRuntimeEngineProvider {
|
||||
CodeRuntimeEngine getCodeRuntimeEngine(Object engineId);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.code.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.graalvm.polyglot.Value;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public class GraalvmToFastJSONUtils {
|
||||
public static Object toFastJsonValue(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (obj instanceof Value) {
|
||||
Value value = (Value) obj;
|
||||
|
||||
if (value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.isBoolean()) {
|
||||
return value.as(Boolean.class);
|
||||
}
|
||||
|
||||
if (value.isNumber()) {
|
||||
if (value.fitsInLong()) {
|
||||
return value.as(Long.class);
|
||||
} else if (value.fitsInDouble()) {
|
||||
return value.as(Double.class);
|
||||
} else {
|
||||
return value.toString(); // e.g., BigInt
|
||||
}
|
||||
}
|
||||
|
||||
if (value.isString()) {
|
||||
return value.as(String.class);
|
||||
}
|
||||
|
||||
if (value.hasArrayElements()) {
|
||||
long size = value.getArraySize();
|
||||
JSONArray array = new JSONArray();
|
||||
for (long i = 0; i < size; i++) {
|
||||
array.add(toFastJsonValue(value.getArrayElement(i)));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
if (value.hasMembers()) {
|
||||
JSONObject object = new JSONObject();
|
||||
Set<String> keys = value.getMemberKeys();
|
||||
for (String key : keys) {
|
||||
Value member = value.getMember(key);
|
||||
// 排除函数
|
||||
if (!member.canExecute()) {
|
||||
object.put(key, toFastJsonValue(member));
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
// 处理标准 Java 类型 ---------------------------------------
|
||||
|
||||
if (obj instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String key = Objects.toString(entry.getKey(), "null");
|
||||
Object convertedValue = toFastJsonValue(entry.getValue());
|
||||
jsonObject.put(key, convertedValue);
|
||||
}
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
if (obj instanceof Collection) {
|
||||
Collection<?> coll = (Collection<?>) obj;
|
||||
JSONArray array = new JSONArray();
|
||||
for (Object item : coll) {
|
||||
array.add(toFastJsonValue(item));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
if (obj.getClass().isArray()) {
|
||||
int length = java.lang.reflect.Array.getLength(obj);
|
||||
JSONArray array = new JSONArray();
|
||||
for (int i = 0; i < length; i++) {
|
||||
Object item = java.lang.reflect.Array.get(obj, i);
|
||||
array.add(toFastJsonValue(item));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
// 基本类型直接返回
|
||||
if (obj instanceof String ||
|
||||
obj instanceof Boolean ||
|
||||
obj instanceof Byte ||
|
||||
obj instanceof Short ||
|
||||
obj instanceof Integer ||
|
||||
obj instanceof Long ||
|
||||
obj instanceof Float ||
|
||||
obj instanceof Double) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 兜底:调用 toString
|
||||
return obj.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为 JSONObject(适合根是对象)
|
||||
*/
|
||||
public static JSONObject toJSONObject(Object obj) {
|
||||
Object result = toFastJsonValue(obj);
|
||||
if (result instanceof JSONObject) {
|
||||
return (JSONObject) result;
|
||||
} else if (result instanceof Map) {
|
||||
return new JSONObject((Map) result);
|
||||
} else {
|
||||
return new JSONObject().fluentPut("value", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.code.impl;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
|
||||
import org.graalvm.polyglot.Context;
|
||||
import org.graalvm.polyglot.HostAccess;
|
||||
import org.graalvm.polyglot.Value;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
|
||||
// 使用 Context.Builder 构建上下文,线程安全
|
||||
private static final Context.Builder CONTEXT_BUILDER = Context.newBuilder("js")
|
||||
.option("engine.WarnInterpreterOnly", "false")
|
||||
.allowHostAccess(HostAccess.ALL) // 允许访问 Java 对象的方法和字段
|
||||
.allowHostClassLookup(className -> false) // 禁止动态加载任意 Java 类
|
||||
.option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(String code, CodeNode node, Chain chain) {
|
||||
try (Context context = CONTEXT_BUILDER.build()) {
|
||||
Value bindings = context.getBindings("js");
|
||||
|
||||
Map<String, Object> all = chain.getState().getMemory();
|
||||
all.forEach((key, value) -> {
|
||||
if (!key.contains(".")) {
|
||||
bindings.putMember(key, JsInteropUtils.wrapJavaValueForJS(context, value));
|
||||
}
|
||||
});
|
||||
|
||||
// 注入参数
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(node);
|
||||
if (parameterValues != null) {
|
||||
for (Map.Entry<String, Object> entry : parameterValues.entrySet()) {
|
||||
bindings.putMember(entry.getKey(), JsInteropUtils.wrapJavaValueForJS(context, entry.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
bindings.putMember("_chain", chain);
|
||||
bindings.putMember("_state", chain.getNodeState(node.getId()));
|
||||
|
||||
|
||||
// 在 JS 中创建 _result 对象
|
||||
context.eval("js", "var _result = {};");
|
||||
|
||||
// 注入 _chain 和 _context
|
||||
bindings.putMember("_chain", chain);
|
||||
bindings.putMember("_state", chain.getNodeState(node.getId()));
|
||||
|
||||
// 执行用户脚本
|
||||
context.eval("js", code);
|
||||
|
||||
Value resultValue = bindings.getMember("_result");
|
||||
|
||||
return GraalvmToFastJSONUtils.toJSONObject(resultValue);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Polyglot JS 脚本执行失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.filestoreage;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
public interface FileStorage {
|
||||
|
||||
String saveFile(InputStream stream, Map<String, String> headers, BaseNode node, Chain chain);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.filestoreage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FileStorageManager {
|
||||
|
||||
public List<FileStorageProvider> providers = new ArrayList<>();
|
||||
|
||||
private static class ManagerHolder {
|
||||
private static final FileStorageManager INSTANCE = new FileStorageManager();
|
||||
}
|
||||
|
||||
private FileStorageManager() {
|
||||
}
|
||||
|
||||
public static FileStorageManager getInstance() {
|
||||
return ManagerHolder.INSTANCE;
|
||||
}
|
||||
|
||||
public void registerProvider(FileStorageProvider provider) {
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
public void removeProvider(FileStorageProvider provider) {
|
||||
providers.remove(provider);
|
||||
}
|
||||
|
||||
public FileStorage getFileStorage() {
|
||||
for (FileStorageProvider provider : providers) {
|
||||
FileStorage fileStorage = provider.getFileStorage();
|
||||
if (fileStorage != null) {
|
||||
return fileStorage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.filestoreage;
|
||||
|
||||
|
||||
public interface FileStorageProvider {
|
||||
FileStorage getFileStorage();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.knowledge;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.KnowledgeNode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface Knowledge {
|
||||
List<Map<String, Object>> search(String keyword, int limit, KnowledgeNode knowledgeNode, Chain chain);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.knowledge;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class KnowledgeManager {
|
||||
|
||||
public List<KnowledgeProvider> providers = new ArrayList<>();
|
||||
|
||||
private static class ManagerHolder {
|
||||
private static final KnowledgeManager INSTANCE = new KnowledgeManager();
|
||||
}
|
||||
|
||||
private KnowledgeManager() {
|
||||
}
|
||||
|
||||
public static KnowledgeManager getInstance() {
|
||||
return ManagerHolder.INSTANCE;
|
||||
}
|
||||
|
||||
public void registerProvider(KnowledgeProvider provider) {
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
public void removeProvider(KnowledgeProvider provider) {
|
||||
providers.remove(provider);
|
||||
}
|
||||
|
||||
public Knowledge getKnowledge(Object knowledgeId) {
|
||||
for (KnowledgeProvider provider : providers) {
|
||||
Knowledge knowledge = provider.getKnowledge(knowledgeId);
|
||||
if (knowledge != null) {
|
||||
return knowledge;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.knowledge;
|
||||
|
||||
public interface KnowledgeProvider {
|
||||
Knowledge getKnowledge(Object id);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.llm;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.LlmNode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Llm接口定义了大语言模型的基本功能规范
|
||||
* <p>
|
||||
* 该接口提供了与大型语言模型交互的标准方法,
|
||||
* 包括文本生成、对话处理等核心功能
|
||||
*/
|
||||
public interface Llm {
|
||||
|
||||
|
||||
/**
|
||||
* 执行聊天对话操作
|
||||
*
|
||||
* @param messageInfo 消息信息对象,包含用户输入的原始消息内容及相关元数据
|
||||
* @param options 聊天配置选项,用于控制对话行为和模型参数
|
||||
* @param llmNode 大语言模型节点,指定使用的具体语言模型实例
|
||||
* @param chain 对话链对象,管理对话历史和上下文状态
|
||||
* @return 返回模型生成的回复字符串
|
||||
*/
|
||||
String chat(MessageInfo messageInfo, ChatOptions options, LlmNode llmNode, Chain chain);
|
||||
|
||||
|
||||
/**
|
||||
* 消息信息类,用于封装消息相关的信息
|
||||
*/
|
||||
class MessageInfo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String message;
|
||||
private String systemMessage;
|
||||
private List<String> images;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getSystemMessage() {
|
||||
return systemMessage;
|
||||
}
|
||||
|
||||
public void setSystemMessage(String systemMessage) {
|
||||
this.systemMessage = systemMessage;
|
||||
}
|
||||
|
||||
public List<String> getImages() {
|
||||
return images;
|
||||
}
|
||||
|
||||
public void setImages(List<String> images) {
|
||||
this.images = images;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatOptions 类用于存储聊天相关的配置选项
|
||||
* 该类实现了Serializable接口,支持序列化操作
|
||||
*/
|
||||
/**
|
||||
* ChatOptions类用于配置聊天模型的参数选项
|
||||
* 实现了Serializable接口,支持序列化
|
||||
*/
|
||||
class ChatOptions implements Serializable {
|
||||
|
||||
private String seed;
|
||||
private Float temperature = 0.8f;
|
||||
private Float topP;
|
||||
private Integer topK;
|
||||
private Integer maxTokens;
|
||||
private List<String> stop;
|
||||
|
||||
/**
|
||||
* 获取随机种子值
|
||||
*
|
||||
* @return 返回随机种子字符串,用于控制生成结果的随机性
|
||||
*/
|
||||
public String getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置随机种子值
|
||||
*
|
||||
* @param seed 随机种子字符串,用于控制生成结果的随机性
|
||||
*/
|
||||
public void setSeed(String seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取温度参数
|
||||
*
|
||||
* @return 返回温度值,控制生成文本的随机性,值越高越随机
|
||||
*/
|
||||
public Float getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置温度参数
|
||||
*
|
||||
* @param temperature 温度值,控制生成文本的随机性,值越高越随机
|
||||
*/
|
||||
public void setTemperature(Float temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Top-P参数
|
||||
*
|
||||
* @return 返回Top-P值,用于 nucleus sampling,控制生成词汇的概率阈值
|
||||
*/
|
||||
public Float getTopP() {
|
||||
return topP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Top-P参数
|
||||
*
|
||||
* @param topP Top-P值,用于 nucleus sampling,控制生成词汇的概率阈值
|
||||
*/
|
||||
public void setTopP(Float topP) {
|
||||
this.topP = topP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Top-K参数
|
||||
*
|
||||
* @return 返回Top-K值,限制每步选择词汇的范围
|
||||
*/
|
||||
public Integer getTopK() {
|
||||
return topK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置Top-K参数
|
||||
*
|
||||
* @param topK Top-K值,限制每步选择词汇的范围
|
||||
*/
|
||||
public void setTopK(Integer topK) {
|
||||
this.topK = topK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大令牌数
|
||||
*
|
||||
* @return 返回最大令牌数,限制生成文本的最大长度
|
||||
*/
|
||||
public Integer getMaxTokens() {
|
||||
return maxTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大令牌数
|
||||
*
|
||||
* @param maxTokens 最大令牌数,限制生成文本的最大长度
|
||||
*/
|
||||
public void setMaxTokens(Integer maxTokens) {
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取停止词列表
|
||||
*
|
||||
* @return 返回停止词字符串列表,当生成文本遇到这些词时会停止生成
|
||||
*/
|
||||
public List<String> getStop() {
|
||||
return stop;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置停止词列表
|
||||
*
|
||||
* @param stop 停止词字符串列表,当生成文本遇到这些词时会停止生成
|
||||
*/
|
||||
public void setStop(List<String> stop) {
|
||||
this.stop = stop;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.llm;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LlmManager {
|
||||
|
||||
public List<LlmProvider> providers = new ArrayList<>();
|
||||
|
||||
private static class ManagerHolder {
|
||||
private static final LlmManager INSTANCE = new LlmManager();
|
||||
}
|
||||
|
||||
private LlmManager() {
|
||||
}
|
||||
|
||||
public static LlmManager getInstance() {
|
||||
return ManagerHolder.INSTANCE;
|
||||
}
|
||||
|
||||
public void registerProvider(LlmProvider provider) {
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
public void removeProvider(LlmProvider provider) {
|
||||
providers.remove(provider);
|
||||
}
|
||||
|
||||
public Llm getChatModel(Object modelId) {
|
||||
for (LlmProvider provider : providers) {
|
||||
Llm llm = provider.getChatModel(modelId);
|
||||
if (llm != null) {
|
||||
return llm;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.llm;
|
||||
|
||||
|
||||
|
||||
public interface LlmProvider {
|
||||
|
||||
Llm getChatModel(Object modelId);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseNode extends Node {
|
||||
|
||||
protected List<Parameter> parameters;
|
||||
protected List<Parameter> outputDefs;
|
||||
|
||||
public void setParameters(List<Parameter> parameters) {
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public List<Parameter> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void addInputParameter(Parameter parameter) {
|
||||
if (parameters == null) {
|
||||
parameters = new java.util.ArrayList<>();
|
||||
}
|
||||
parameters.add(parameter);
|
||||
}
|
||||
|
||||
|
||||
public List<Parameter> getOutputDefs() {
|
||||
return outputDefs;
|
||||
}
|
||||
|
||||
public void setOutputDefs(List<Parameter> outputDefs) {
|
||||
this.outputDefs = outputDefs;
|
||||
}
|
||||
|
||||
public void addOutputDef(Parameter parameter) {
|
||||
if (outputDefs == null) {
|
||||
outputDefs = new java.util.ArrayList<>();
|
||||
}
|
||||
outputDefs.add(parameter);
|
||||
}
|
||||
|
||||
public void addOutputDefs(Collection<Parameter> parameters) {
|
||||
if (outputDefs == null) {
|
||||
outputDefs = new java.util.ArrayList<>();
|
||||
}
|
||||
outputDefs.addAll(parameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
||||
import com.easyagents.flow.core.code.CodeRuntimeEngineManager;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.util.TextTemplate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CodeNode extends BaseNode {
|
||||
protected String engine;
|
||||
protected String code;
|
||||
|
||||
public String getEngine() {
|
||||
return engine;
|
||||
}
|
||||
|
||||
public void setEngine(String engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
if (StringUtil.noText(code)) {
|
||||
throw new IllegalArgumentException("code is empty");
|
||||
}
|
||||
|
||||
ChainState chainState = chain.getState();
|
||||
List<Map<String, Object>> variables = Arrays.asList(chainState.resolveParameters(this), chainState.getEnvMap());
|
||||
String newCode = TextTemplate.of(code).formatToString(variables);
|
||||
|
||||
CodeRuntimeEngine codeRuntimeEngine = CodeRuntimeEngineManager.getInstance().getCodeRuntimeEngine(this.engine);
|
||||
return codeRuntimeEngine.execute(newCode, this, chain);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainSuspendException;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.RefType;
|
||||
import com.easyagents.flow.core.chain.repository.ChainStateField;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ConfirmNode extends BaseNode {
|
||||
|
||||
private String message;
|
||||
private List<Parameter> confirms;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public List<Parameter> getConfirms() {
|
||||
return confirms;
|
||||
}
|
||||
|
||||
public void setConfirms(List<Parameter> confirms) {
|
||||
if (confirms != null) {
|
||||
for (Parameter confirm : confirms) {
|
||||
confirm.setRefType(RefType.INPUT);
|
||||
confirm.setRequired(true); // 必填,才能正确通过 getParameterValuesOnly 获取参数值
|
||||
confirm.setName(confirm.getName());
|
||||
}
|
||||
}
|
||||
this.confirms = confirms;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
|
||||
List<Parameter> confirmParameters = new ArrayList<>();
|
||||
addConfirmParameter(confirmParameters);
|
||||
|
||||
if (confirms != null) {
|
||||
for (Parameter confirm : confirms) {
|
||||
Parameter clone = confirm.clone();
|
||||
clone.setName(confirm.getName() + "__" + getId());
|
||||
clone.setRefType(RefType.INPUT);
|
||||
confirmParameters.add(clone);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> values;
|
||||
try {
|
||||
values = chain.getState().resolveParameters(this, confirmParameters);
|
||||
// 移除 confirm 参数,方便在其他节点二次确认,或者在 for 循环中第二次获取
|
||||
chain.updateStateSafely(state -> {
|
||||
for (Parameter confirmParameter : confirmParameters) {
|
||||
state.getMemory().remove(confirmParameter.getName());
|
||||
}
|
||||
return EnumSet.of(ChainStateField.MEMORY);
|
||||
});
|
||||
} catch (ChainSuspendException e) {
|
||||
chain.updateStateSafely(state -> {
|
||||
state.setMessage(message);
|
||||
return EnumSet.of(ChainStateField.MESSAGE);
|
||||
});
|
||||
|
||||
if (confirms != null) {
|
||||
List<Parameter> newParameters = new ArrayList<>();
|
||||
for (Parameter confirm : confirms) {
|
||||
Parameter clone = confirm.clone();
|
||||
clone.setName(confirm.getName() + "__" + getId());
|
||||
clone.setRefType(RefType.REF); // 固定为 REF
|
||||
newParameters.add(clone);
|
||||
}
|
||||
|
||||
// 获取参数值,不会触发 ChainSuspendException 错误
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(this, newParameters, null, true);
|
||||
|
||||
// 设置 enums,方便前端给用户进行选择
|
||||
for (Parameter confirmParameter : confirmParameters) {
|
||||
if (confirmParameter.getEnums() == null) {
|
||||
Object enumsObject = parameterValues.get(confirmParameter.getName());
|
||||
confirmParameter.setEnumsObject(enumsObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
Map<String, Object> results = new HashMap<>(values.size());
|
||||
values.forEach((key, value) -> {
|
||||
int index = key.lastIndexOf("__");
|
||||
if (index >= 0) {
|
||||
results.put(key.substring(0, index), value);
|
||||
} else {
|
||||
results.put(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
private void addConfirmParameter(List<Parameter> parameters) {
|
||||
// “确认 和 取消” 的参数
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setRefType(RefType.INPUT);
|
||||
parameter.setId("confirm");
|
||||
parameter.setName("confirm__" + getId());
|
||||
parameter.setRequired(true);
|
||||
|
||||
List<Object> selectionData = new ArrayList<>();
|
||||
selectionData.add("yes");
|
||||
selectionData.add("no");
|
||||
|
||||
parameter.setEnums(selectionData);
|
||||
parameter.setContentType("text");
|
||||
parameter.setFormType("confirm");
|
||||
parameters.add(parameter);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class EndNode extends BaseNode {
|
||||
private boolean normal = true;
|
||||
private String message;
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public boolean isNormal() {
|
||||
return normal;
|
||||
}
|
||||
|
||||
public void setNormal(boolean normal) {
|
||||
this.normal = normal;
|
||||
}
|
||||
|
||||
public EndNode() {
|
||||
this.name = "end";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
|
||||
Map<String, Object> output = new HashMap<>();
|
||||
if (normal) {
|
||||
output.put(ChainConsts.CHAIN_STATE_STATUS_KEY, ChainStatus.SUCCEEDED);
|
||||
} else {
|
||||
output.put(ChainConsts.CHAIN_STATE_STATUS_KEY, ChainStatus.FAILED);
|
||||
}
|
||||
|
||||
if (StringUtil.hasText(message)) {
|
||||
output.put(ChainConsts.CHAIN_STATE_MESSAGE_KEY, message);
|
||||
}
|
||||
|
||||
if (this.outputDefs != null) {
|
||||
for (Parameter outputDef : this.outputDefs) {
|
||||
if (outputDef.getRefType() == RefType.REF) {
|
||||
output.put(outputDef.getName(), chain.getState().resolveValue(outputDef.getRef()));
|
||||
} else if (outputDef.getRefType() == RefType.INPUT) {
|
||||
output.put(outputDef.getName(), outputDef.getRef());
|
||||
} else if (outputDef.getRefType() == RefType.FIXED) {
|
||||
output.put(outputDef.getName(), StringUtil.getFirstWithText(outputDef.getValue(), outputDef.getDefaultValue()));
|
||||
}
|
||||
// default is ref type
|
||||
else if (StringUtil.hasText(outputDef.getRef())) {
|
||||
output.put(outputDef.getName(), chain.getState().resolveValue(outputDef.getRef()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EndNode{" +
|
||||
"normal=" + normal +
|
||||
", message='" + message + '\'' +
|
||||
", parameters=" + parameters +
|
||||
", outputDefs=" + outputDefs +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", condition=" + condition +
|
||||
", validator=" + validator +
|
||||
", loopEnable=" + loopEnable +
|
||||
", loopIntervalMs=" + loopIntervalMs +
|
||||
", loopBreakCondition=" + loopBreakCondition +
|
||||
", maxLoopCount=" + maxLoopCount +
|
||||
", retryEnable=" + retryEnable +
|
||||
", resetRetryCountAfterNormal=" + resetRetryCountAfterNormal +
|
||||
", maxRetryCount=" + maxRetryCount +
|
||||
", retryIntervalMs=" + retryIntervalMs +
|
||||
", computeCostExpr='" + computeCostExpr + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.DataType;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.filestoreage.FileStorage;
|
||||
import com.easyagents.flow.core.filestoreage.FileStorageManager;
|
||||
import com.easyagents.flow.core.util.OkHttpClientUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.util.TextTemplate;
|
||||
import okhttp3.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class HttpNode extends BaseNode {
|
||||
|
||||
private String url;
|
||||
private String method;
|
||||
|
||||
private List<Parameter> headers;
|
||||
|
||||
private String bodyType;
|
||||
private List<Parameter> formData;
|
||||
private List<Parameter> formUrlencoded;
|
||||
private String bodyJson;
|
||||
private String rawBody;
|
||||
|
||||
public static String mapToQueryString(Map<String, Object> map) {
|
||||
if (map == null || map.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (String key : map.keySet()) {
|
||||
if (StringUtil.noText(key)) {
|
||||
continue;
|
||||
}
|
||||
if (stringBuilder.length() > 0) {
|
||||
stringBuilder.append("&");
|
||||
}
|
||||
stringBuilder.append(key.trim());
|
||||
stringBuilder.append("=");
|
||||
Object value = map.get(key);
|
||||
stringBuilder.append(value == null ? "" : urlEncode(value.toString().trim()));
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
public static String urlEncode(String string) {
|
||||
try {
|
||||
return URLEncoder.encode(string, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public List<Parameter> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public void setHeaders(List<Parameter> headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public String getBodyType() {
|
||||
return bodyType;
|
||||
}
|
||||
|
||||
public void setBodyType(String bodyType) {
|
||||
this.bodyType = bodyType;
|
||||
}
|
||||
|
||||
public List<Parameter> getFormData() {
|
||||
return formData;
|
||||
}
|
||||
|
||||
public void setFormData(List<Parameter> formData) {
|
||||
this.formData = formData;
|
||||
}
|
||||
|
||||
public List<Parameter> getFormUrlencoded() {
|
||||
return formUrlencoded;
|
||||
}
|
||||
|
||||
public void setFormUrlencoded(List<Parameter> formUrlencoded) {
|
||||
this.formUrlencoded = formUrlencoded;
|
||||
}
|
||||
|
||||
public String getBodyJson() {
|
||||
return bodyJson;
|
||||
}
|
||||
|
||||
public void setBodyJson(String bodyJson) {
|
||||
this.bodyJson = bodyJson;
|
||||
}
|
||||
|
||||
public String getRawBody() {
|
||||
return rawBody;
|
||||
}
|
||||
|
||||
public void setRawBody(String rawBody) {
|
||||
this.rawBody = rawBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
int maxRetry = 5;
|
||||
long retryInterval = 2000L;
|
||||
|
||||
int attempt = 0;
|
||||
Throwable lastError = null;
|
||||
|
||||
while (attempt < maxRetry) {
|
||||
attempt++;
|
||||
|
||||
try {
|
||||
return doExecute(chain);
|
||||
} catch (Throwable ex) {
|
||||
|
||||
lastError = ex;
|
||||
|
||||
// 判断是否需要重试
|
||||
if (!shouldRetry(ex)) {
|
||||
throw wrapAsRuntime(ex, attempt);
|
||||
}
|
||||
|
||||
try {
|
||||
long waitMs = Math.min(
|
||||
retryInterval * (1L << (attempt - 1)),
|
||||
10_000L // 最大 10 秒
|
||||
);
|
||||
Thread.sleep(waitMs);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("HTTP retry interrupted", ie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 理论上不会走到这里
|
||||
throw wrapAsRuntime(lastError, attempt);
|
||||
}
|
||||
|
||||
|
||||
protected boolean shouldRetry(Throwable ex) {
|
||||
if (ex instanceof HttpServerErrorException) {
|
||||
int code = ((HttpServerErrorException) ex).getStatusCode();
|
||||
return code == 503 || code == 504; // 只对特定 5xx 重试
|
||||
}
|
||||
|
||||
// 1. IO 异常(超时、连接失败、Socket 问题)
|
||||
if (ex instanceof IOException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. 包装过的异常
|
||||
Throwable cause = ex.getCause();
|
||||
return cause instanceof IOException;
|
||||
}
|
||||
|
||||
private RuntimeException wrapAsRuntime(Throwable ex, int attempt) {
|
||||
if (ex instanceof RuntimeException) {
|
||||
return (RuntimeException) ex;
|
||||
}
|
||||
return new RuntimeException(
|
||||
String.format("HttpNode[%s] failed after %d attempt(s)", getName(), attempt),
|
||||
ex
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> doExecute(Chain chain) throws IOException {
|
||||
|
||||
Map<String, Object> argsMap = chain.getState().resolveParameters(this);
|
||||
String newUrl = TextTemplate.of(url).formatToString(Arrays.asList(argsMap, chain.getState().getEnvMap()));
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(newUrl);
|
||||
|
||||
Map<String, Object> headersMap = chain.getState().resolveParameters(this, headers, argsMap);
|
||||
headersMap.forEach((s, o) -> reqBuilder.addHeader(s, String.valueOf(o)));
|
||||
|
||||
if (StringUtil.noText(method) || "GET".equalsIgnoreCase(method)) {
|
||||
reqBuilder.method("GET", null);
|
||||
} else {
|
||||
reqBuilder.method(method.toUpperCase(), getRequestBody(chain, argsMap));
|
||||
}
|
||||
|
||||
OkHttpClient okHttpClient = OkHttpClientUtil.buildDefaultClient();
|
||||
try (Response response = okHttpClient.newCall(reqBuilder.build()).execute()) {
|
||||
|
||||
// 服务器异常
|
||||
if (response.code() >= 500 && response.code() < 600) {
|
||||
throw new HttpServerErrorException(response.code(), response.message());
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("statusCode", response.code());
|
||||
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
Headers headers = response.headers();
|
||||
for (String name : headers.names()) {
|
||||
responseHeaders.put(name, response.header(name));
|
||||
}
|
||||
result.put("headers", responseHeaders);
|
||||
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
result.put("body", null);
|
||||
return result;
|
||||
}
|
||||
|
||||
DataType bodyDataType = null;
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
if (outputDefs != null) {
|
||||
for (Parameter outputDef : outputDefs) {
|
||||
if ("body".equalsIgnoreCase(outputDef.getName())) {
|
||||
bodyDataType = outputDef.getDataType();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyDataType == null) {
|
||||
result.put("body", body.string());
|
||||
} else if (bodyDataType == DataType.Object || bodyDataType.getValue().startsWith("Array")) {
|
||||
result.put("body", JSON.parse(body.string()));
|
||||
} else if (bodyDataType == DataType.File) {
|
||||
try (InputStream stream = body.byteStream()) {
|
||||
FileStorage fileStorage = FileStorageManager.getInstance().getFileStorage();
|
||||
String fileUrl = fileStorage.saveFile(stream, responseHeaders, this, chain);
|
||||
result.put("body", fileUrl);
|
||||
}
|
||||
} else {
|
||||
result.put("body", body.string());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private RequestBody getRequestBody(Chain chain, Map<String, Object> formatArgs) {
|
||||
if ("json".equals(bodyType)) {
|
||||
String bodyJsonString = TextTemplate.of(bodyJson).formatToString(formatArgs, true);
|
||||
JSONObject jsonObject = JSON.parseObject(bodyJsonString);
|
||||
return RequestBody.create(jsonObject.toString(), MediaType.parse("application/json"));
|
||||
}
|
||||
|
||||
if ("x-www-form-urlencoded".equals(bodyType)) {
|
||||
Map<String, Object> formUrlencodedMap = chain.getState().resolveParameters(this, formUrlencoded);
|
||||
String bodyString = mapToQueryString(formUrlencodedMap);
|
||||
return RequestBody.create(bodyString, MediaType.parse("application/x-www-form-urlencoded"));
|
||||
}
|
||||
|
||||
if ("form-data".equals(bodyType)) {
|
||||
Map<String, Object> formDataMap = chain.getState().resolveParameters(this, formData, formatArgs);
|
||||
|
||||
MultipartBody.Builder builder = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM);
|
||||
|
||||
formDataMap.forEach((s, o) -> {
|
||||
// if (o instanceof File) {
|
||||
// File f = (File) o;
|
||||
// RequestBody body = RequestBody.create(f, MediaType.parse("application/octet-stream"));
|
||||
// builder.addFormDataPart(s, f.getName(), body);
|
||||
// } else if (o instanceof InputStream) {
|
||||
// RequestBody body = new HttpClient.InputStreamRequestBody(MediaType.parse("application/octet-stream"), (InputStream) o);
|
||||
// builder.addFormDataPart(s, s, body);
|
||||
// } else if (o instanceof byte[]) {
|
||||
// builder.addFormDataPart(s, s, RequestBody.create((byte[]) o));
|
||||
// } else {
|
||||
// builder.addFormDataPart(s, String.valueOf(o));
|
||||
// }
|
||||
builder.addFormDataPart(s, String.valueOf(o));
|
||||
});
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
if ("raw".equals(bodyType)) {
|
||||
String rawBodyString = TextTemplate.of(rawBody).formatToString(Arrays.asList(formatArgs, chain.getState().getEnvMap()));
|
||||
return RequestBody.create(rawBodyString, null);
|
||||
}
|
||||
//none
|
||||
return RequestBody.create("", null);
|
||||
}
|
||||
|
||||
public static class HttpServerErrorException extends IOException {
|
||||
private final int statusCode;
|
||||
|
||||
public HttpServerErrorException(int statusCode, String message) {
|
||||
super("HTTP " + statusCode + ": " + message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpNode{" +
|
||||
"url='" + url + '\'' +
|
||||
", method='" + method + '\'' +
|
||||
", headers=" + headers +
|
||||
", bodyType='" + bodyType + '\'' +
|
||||
", formData=" + formData +
|
||||
", formUrlencoded=" + formUrlencoded +
|
||||
", bodyJson='" + bodyJson + '\'' +
|
||||
", rawBody='" + rawBody + '\'' +
|
||||
", parameters=" + parameters +
|
||||
", outputDefs=" + outputDefs +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", condition=" + condition +
|
||||
", validator=" + validator +
|
||||
", loopEnable=" + loopEnable +
|
||||
", loopIntervalMs=" + loopIntervalMs +
|
||||
", loopBreakCondition=" + loopBreakCondition +
|
||||
", maxLoopCount=" + maxLoopCount +
|
||||
", retryEnable=" + retryEnable +
|
||||
", resetRetryCountAfterNormal=" + resetRetryCountAfterNormal +
|
||||
", maxRetryCount=" + maxRetryCount +
|
||||
", retryIntervalMs=" + retryIntervalMs +
|
||||
", computeCostExpr='" + computeCostExpr + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.knowledge.Knowledge;
|
||||
import com.easyagents.flow.core.knowledge.KnowledgeManager;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.util.TextTemplate;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class KnowledgeNode extends BaseNode {
|
||||
|
||||
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(KnowledgeNode.class);
|
||||
|
||||
private Object knowledgeId;
|
||||
private String keyword;
|
||||
private String limit;
|
||||
|
||||
public Object getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(Object knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public String getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(String limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> argsMap = chain.getState().resolveParameters(this);
|
||||
String realKeyword = TextTemplate.of(keyword).formatToString(Arrays.asList(argsMap, chain.getState().getEnvMap()));
|
||||
String realLimitString = TextTemplate.of(limit).formatToString(Arrays.asList(argsMap, chain.getState().getEnvMap()));
|
||||
int realLimit = 10;
|
||||
if (StringUtil.hasText(realLimitString)) {
|
||||
try {
|
||||
realLimit = Integer.parseInt(realLimitString);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
Knowledge knowledge = KnowledgeManager.getInstance().getKnowledge(knowledgeId);
|
||||
|
||||
if (knowledge == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result = knowledge.search(realKeyword, realLimit, this, chain);
|
||||
return Maps.of("documents", result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KnowledgeNode{" +
|
||||
"knowledgeId=" + knowledgeId +
|
||||
", keyword='" + keyword + '\'' +
|
||||
", limit='" + limit + '\'' +
|
||||
", parameters=" + parameters +
|
||||
", outputDefs=" + outputDefs +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", condition=" + condition +
|
||||
", validator=" + validator +
|
||||
", loopEnable=" + loopEnable +
|
||||
", loopIntervalMs=" + loopIntervalMs +
|
||||
", loopBreakCondition=" + loopBreakCondition +
|
||||
", maxLoopCount=" + maxLoopCount +
|
||||
", retryEnable=" + retryEnable +
|
||||
", resetRetryCountAfterNormal=" + resetRetryCountAfterNormal +
|
||||
", maxRetryCount=" + maxRetryCount +
|
||||
", retryIntervalMs=" + retryIntervalMs +
|
||||
", computeCostExpr='" + computeCostExpr + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.llm.Llm;
|
||||
import com.easyagents.flow.core.llm.LlmManager;
|
||||
import com.easyagents.flow.core.util.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class LlmNode extends BaseNode {
|
||||
|
||||
protected String llmId;
|
||||
protected Llm.ChatOptions chatOptions;
|
||||
protected String userPrompt;
|
||||
|
||||
protected String systemPrompt;
|
||||
protected String outType = "text"; //text markdown json
|
||||
protected List<Parameter> images;
|
||||
|
||||
public LlmNode() {
|
||||
}
|
||||
|
||||
public String getLlmId() {
|
||||
return llmId;
|
||||
}
|
||||
|
||||
public void setLlmId(String llmId) {
|
||||
this.llmId = llmId;
|
||||
}
|
||||
|
||||
public String getUserPrompt() {
|
||||
return userPrompt;
|
||||
}
|
||||
|
||||
public void setUserPrompt(String userPrompt) {
|
||||
this.userPrompt = userPrompt;
|
||||
}
|
||||
|
||||
public String getSystemPrompt() {
|
||||
return systemPrompt;
|
||||
}
|
||||
|
||||
public void setSystemPrompt(String systemPrompt) {
|
||||
this.systemPrompt = systemPrompt;
|
||||
}
|
||||
|
||||
public Llm.ChatOptions getChatOptions() {
|
||||
return chatOptions;
|
||||
}
|
||||
|
||||
public void setChatOptions(Llm.ChatOptions chatOptions) {
|
||||
this.chatOptions = chatOptions;
|
||||
}
|
||||
|
||||
public String getOutType() {
|
||||
return outType;
|
||||
}
|
||||
|
||||
public void setOutType(String outType) {
|
||||
this.outType = outType;
|
||||
}
|
||||
|
||||
public List<Parameter> getImages() {
|
||||
return images;
|
||||
}
|
||||
|
||||
public void setImages(List<Parameter> images) {
|
||||
this.images = images;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> parameterValues = chain.getState().resolveParameters(this);
|
||||
|
||||
if (StringUtil.noText(userPrompt)) {
|
||||
throw new RuntimeException("Can not find user prompt");
|
||||
}
|
||||
|
||||
String userPromptString = TextTemplate.of(userPrompt).formatToString(Arrays.asList(parameterValues, chain.getState().getEnvMap()));
|
||||
|
||||
|
||||
Llm llm = LlmManager.getInstance().getChatModel(this.llmId);
|
||||
if (llm == null) {
|
||||
throw new RuntimeException("Can not find llm: " + this.llmId);
|
||||
}
|
||||
|
||||
String systemPromptString = TextTemplate.of(this.systemPrompt).formatToString(Arrays.asList(parameterValues, chain.getState().getEnvMap()));
|
||||
|
||||
Llm.MessageInfo messageInfo = new Llm.MessageInfo();
|
||||
messageInfo.setMessage(userPromptString);
|
||||
messageInfo.setSystemMessage(systemPromptString);
|
||||
|
||||
if (images != null && !images.isEmpty()) {
|
||||
Map<String, Object> filesMap = chain.getState().resolveParameters(this, images);
|
||||
List<String> imagesUrls = new ArrayList<>();
|
||||
filesMap.forEach((s, o) -> {
|
||||
if (o instanceof String) {
|
||||
imagesUrls.add((String) o);
|
||||
} else if (o instanceof File) {
|
||||
byte[] bytes = IOUtil.readBytes((File) o);
|
||||
String base64 = Base64.getEncoder().encodeToString(bytes);
|
||||
imagesUrls.add(base64);
|
||||
}
|
||||
});
|
||||
messageInfo.setImages(imagesUrls);
|
||||
}
|
||||
|
||||
|
||||
String responseContent = llm.chat(messageInfo, chatOptions, this, chain);
|
||||
|
||||
if (StringUtil.noText(responseContent)) {
|
||||
throw new RuntimeException("Can not get response from llm");
|
||||
} else {
|
||||
responseContent = responseContent.trim();
|
||||
}
|
||||
|
||||
|
||||
if ("json".equalsIgnoreCase(outType)) {
|
||||
Object jsonObjectOrArray;
|
||||
try {
|
||||
jsonObjectOrArray = JSON.parse(unWrapMarkdown(responseContent));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Can not parse json: " + responseContent + " " + e.getMessage());
|
||||
}
|
||||
|
||||
if (CollectionUtil.noItems(this.outputDefs)) {
|
||||
return Maps.of("root", jsonObjectOrArray);
|
||||
} else {
|
||||
Parameter parameter = this.outputDefs.get(0);
|
||||
return Maps.of(parameter.getName(), jsonObjectOrArray);
|
||||
}
|
||||
} else {
|
||||
if (CollectionUtil.noItems(this.outputDefs)) {
|
||||
return Maps.of("output", responseContent);
|
||||
} else {
|
||||
Parameter parameter = this.outputDefs.get(0);
|
||||
return Maps.of(parameter.getName(), responseContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移除 ``` 或者 ```json 等
|
||||
*
|
||||
* @param markdown json内容
|
||||
* @return 方法 json 内容
|
||||
*/
|
||||
public static String unWrapMarkdown(String markdown) {
|
||||
// 移除开头的 ```json 或 ```
|
||||
if (markdown.startsWith("```")) {
|
||||
int newlineIndex = markdown.indexOf('\n');
|
||||
if (newlineIndex != -1) {
|
||||
markdown = markdown.substring(newlineIndex + 1);
|
||||
} else {
|
||||
// 如果没有换行符,直接去掉 ``` 部分
|
||||
markdown = markdown.substring(3);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除结尾的 ```
|
||||
if (markdown.endsWith("```")) {
|
||||
markdown = markdown.substring(0, markdown.length() - 3);
|
||||
}
|
||||
return markdown.trim();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LlmNode{" +
|
||||
"llmId='" + llmId + '\'' +
|
||||
", chatOptions=" + chatOptions +
|
||||
", userPrompt='" + userPrompt + '\'' +
|
||||
", systemPrompt='" + systemPrompt + '\'' +
|
||||
", outType='" + outType + '\'' +
|
||||
", images=" + images +
|
||||
", parameters=" + parameters +
|
||||
", outputDefs=" + outputDefs +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", condition=" + condition +
|
||||
", validator=" + validator +
|
||||
", loopEnable=" + loopEnable +
|
||||
", loopIntervalMs=" + loopIntervalMs +
|
||||
", loopBreakCondition=" + loopBreakCondition +
|
||||
", maxLoopCount=" + maxLoopCount +
|
||||
", retryEnable=" + retryEnable +
|
||||
", resetRetryCountAfterNormal=" + resetRetryCountAfterNormal +
|
||||
", maxRetryCount=" + maxRetryCount +
|
||||
", retryIntervalMs=" + retryIntervalMs +
|
||||
", computeCostExpr='" + computeCostExpr + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.chain.repository.ChainStateField;
|
||||
import com.easyagents.flow.core.chain.repository.NodeStateField;
|
||||
import com.easyagents.flow.core.chain.runtime.Trigger;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerContext;
|
||||
import com.easyagents.flow.core.chain.runtime.TriggerType;
|
||||
import com.easyagents.flow.core.util.IterableUtil;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class LoopNode extends BaseNode {
|
||||
|
||||
private Parameter loopVar;
|
||||
|
||||
public Parameter getLoopVar() {
|
||||
return loopVar;
|
||||
}
|
||||
|
||||
public void setLoopVar(Parameter loopVar) {
|
||||
this.loopVar = loopVar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Trigger prevTrigger = TriggerContext.getCurrentTrigger();
|
||||
Deque<LoopContext> loopStack = getOrCreateLoopStack(chain);
|
||||
|
||||
LoopContext loopContext;
|
||||
|
||||
// 判断是否是首次进入该 LoopNode(即不是由子节点返回)
|
||||
TriggerType triggerType = prevTrigger.getType();
|
||||
boolean isFirstEntry = triggerType != TriggerType.PARENT && triggerType != TriggerType.SELF;
|
||||
|
||||
if (isFirstEntry) {
|
||||
// 首次触发:创建新的 LoopContext 并压入堆栈
|
||||
loopContext = new LoopContext();
|
||||
loopContext.currentIndex = 0;
|
||||
loopContext.subResult = new HashMap<>();
|
||||
// 保存原始触发上下文(用于循环结束后恢复)
|
||||
loopStack.offerLast(loopContext);
|
||||
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(buildLoopStackId(), loopStack);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
|
||||
if (loopStack.size() > 1) {
|
||||
// 不执行,等等其他节点唤起
|
||||
return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
|
||||
.set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
}
|
||||
}
|
||||
// 由子节点返回:从堆栈低部获取当前循环上下文
|
||||
else {
|
||||
if (loopStack.isEmpty()) {
|
||||
throw new IllegalStateException("Loop stack is empty when returning from child node.");
|
||||
}
|
||||
loopContext = loopStack.peekFirst();
|
||||
}
|
||||
|
||||
|
||||
// LoopContext loopContext = getLoopContext(prevTrigger, chain);
|
||||
// int triggerLoopIndex = getTriggerLoopIndex(prevTrigger);
|
||||
//
|
||||
// if (loopContext.currentIndex != triggerLoopIndex) {
|
||||
// // 不执行,子流程有分叉,已经被其他的分叉节点触发了
|
||||
// return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
|
||||
// .set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
// }
|
||||
|
||||
Map<String, Object> loopVars = chain.getState().resolveParameters(this, Collections.singletonList(loopVar));
|
||||
Object loopValue = loopVars.get(loopVar.getName());
|
||||
|
||||
int shouldLoopCount;
|
||||
if (loopValue instanceof Iterable) {
|
||||
shouldLoopCount = IterableUtil.size((Iterable<?>) loopValue);
|
||||
} else if (loopValue instanceof Number || (loopValue instanceof String && StringUtil.isNumeric(loopValue.toString()))) {
|
||||
shouldLoopCount = loopValue instanceof Number ? ((Number) loopValue).intValue() : Integer.parseInt(loopValue.toString().trim());
|
||||
} else {
|
||||
throw new IllegalArgumentException("loopValue must be Iterable or Number or String, but loopValue is \"" + loopValue + "\"");
|
||||
}
|
||||
|
||||
// 不是第一次执行,合并结果到 subResult
|
||||
if (loopContext.currentIndex != 0) {
|
||||
ChainState subState = chain.getState();
|
||||
mergeResult(loopContext.subResult, subState);
|
||||
}
|
||||
|
||||
|
||||
// 执行的次数够了, 恢复父级触发
|
||||
if (loopContext.currentIndex >= shouldLoopCount) {
|
||||
loopStack.pollFirst(); // 移除最顶部部的 LoopContext
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
ConcurrentHashMap<String, Object> memory = state.getMemory();
|
||||
memory.put(buildLoopStackId(), loopStack);
|
||||
memory.remove(this.id + ".index");
|
||||
memory.remove(this.id + ".loopItem");
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
if (!loopStack.isEmpty()) {
|
||||
chain.scheduleNode(this, null, TriggerType.SELF, 0);
|
||||
}
|
||||
return loopContext.subResult;
|
||||
}
|
||||
|
||||
int loopIndex = loopContext.currentIndex;
|
||||
loopContext.currentIndex++;
|
||||
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(buildLoopStackId(), loopStack);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
|
||||
|
||||
if (loopValue instanceof Iterable) {
|
||||
Object loopItem = IterableUtil.get((Iterable<?>) loopValue, loopIndex);
|
||||
executeLoopChain(chain, loopContext, loopItem);
|
||||
} else if (loopValue instanceof Number || (loopValue instanceof String && StringUtil.isNumeric(loopValue.toString()))) {
|
||||
executeLoopChain(chain, loopContext, loopIndex);
|
||||
} else {
|
||||
throw new IllegalArgumentException("loopValue must be Iterable or Number or String, but loopValue is \"" + loopValue + "\"");
|
||||
}
|
||||
|
||||
// 禁用调度下个节点
|
||||
return Maps.of(ChainConsts.SCHEDULE_NEXT_NODE_DISABLED_KEY, true)
|
||||
.set(ChainConsts.NODE_STATE_STATUS_KEY, NodeStatus.RUNNING);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取或创建当前节点的 LoopContext 堆栈(每个 LoopNode 实例独立)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Deque<LoopContext> getOrCreateLoopStack(Chain chain) {
|
||||
NodeState nodeState = chain.getNodeState(this.id);
|
||||
String key = buildLoopStackId();
|
||||
Object stackObj = nodeState.getMemory().get(key);
|
||||
Deque<LoopContext> stack;
|
||||
if (stackObj instanceof Deque) {
|
||||
stack = (Deque<LoopContext>) stackObj;
|
||||
} else {
|
||||
stack = new ArrayDeque<>();
|
||||
chain.updateNodeStateSafely(this.id, state -> {
|
||||
state.getMemory().put(key, stack);
|
||||
return EnumSet.of(NodeStateField.MEMORY);
|
||||
});
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
|
||||
private void executeLoopChain(Chain chain, LoopContext loopContext, Object loopItem) {
|
||||
|
||||
chain.updateStateSafely(state -> {
|
||||
ConcurrentHashMap<String, Object> memory = state.getMemory();
|
||||
memory.put(this.id + ".index", (loopContext.currentIndex - 1));
|
||||
memory.put(this.id + ".loopItem", loopItem);
|
||||
return EnumSet.of(ChainStateField.MEMORY);
|
||||
});
|
||||
|
||||
|
||||
ChainDefinition definition = chain.getDefinition();
|
||||
List<Edge> outwardEdges = definition.getOutwardEdge(this.id);
|
||||
for (Edge edge : outwardEdges) {
|
||||
Node childNode = definition.getNodeById(edge.getTarget());
|
||||
if (childNode.getParentId() != null && childNode.getParentId().equals(this.id)) {
|
||||
chain.scheduleNode(childNode, edge.getId(), TriggerType.CHILD, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 把子流程执行的结果填充到主流程的输出参数中
|
||||
*
|
||||
* @param toResult 主流程的输出参数
|
||||
* @param subState 子流程的
|
||||
*/
|
||||
private void mergeResult(Map<String, Object> toResult, ChainState subState) {
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
if (outputDefs != null) {
|
||||
for (Parameter outputDef : outputDefs) {
|
||||
Object value = null;
|
||||
|
||||
//引用
|
||||
if (outputDef.getRefType() == RefType.REF) {
|
||||
value = subState.resolveValue(outputDef.getRef());
|
||||
}
|
||||
//固定值
|
||||
else if (outputDef.getRefType() == RefType.FIXED) {
|
||||
value = outputDef.getValue();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") List<Object> existList = (List<Object>) toResult.get(outputDef.getName());
|
||||
if (existList == null) {
|
||||
existList = new ArrayList<>();
|
||||
}
|
||||
existList.add(value);
|
||||
toResult.put(outputDef.getName(), existList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String buildLoopStackId() {
|
||||
return this.getId() + "__loop__context";
|
||||
}
|
||||
|
||||
|
||||
public static class LoopContext implements Serializable {
|
||||
int currentIndex;
|
||||
Map<String, Object> subResult;
|
||||
|
||||
public int getCurrentIndex() {
|
||||
return currentIndex;
|
||||
}
|
||||
|
||||
public void setCurrentIndex(int currentIndex) {
|
||||
this.currentIndex = currentIndex;
|
||||
}
|
||||
|
||||
public Map<String, Object> getSubResult() {
|
||||
return subResult;
|
||||
}
|
||||
|
||||
public void setSubResult(Map<String, Object> subResult) {
|
||||
this.subResult = subResult;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.searchengine.SearchEngine;
|
||||
import com.easyagents.flow.core.searchengine.SearchEngineManager;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.util.TextTemplate;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SearchEngineNode extends BaseNode {
|
||||
|
||||
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(SearchEngineNode.class);
|
||||
|
||||
private String engine;
|
||||
private String limit;
|
||||
private String keyword;
|
||||
|
||||
public String getEngine() {
|
||||
return engine;
|
||||
}
|
||||
|
||||
public void setEngine(String engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public String getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(String limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> argsMap = chain.getState().resolveParameters(this);
|
||||
String realKeyword = TextTemplate.of(keyword).formatToString(Arrays.asList(argsMap, chain.getState().getEnvMap()));
|
||||
String realLimitString = TextTemplate.of(limit).formatToString(Arrays.asList(argsMap, chain.getState().getEnvMap()));
|
||||
int realLimit = 10;
|
||||
if (StringUtil.hasText(realLimitString)) {
|
||||
try {
|
||||
realLimit = Integer.parseInt(realLimitString);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
SearchEngine searchEngine = SearchEngineManager.getInstance().geSearchEngine(engine);
|
||||
|
||||
if (searchEngine == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> result = searchEngine.search(realKeyword, realLimit, this, chain);
|
||||
return Maps.of("documents", result);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SearchEngineNode{" +
|
||||
"engine='" + engine + '\'' +
|
||||
", limit='" + limit + '\'' +
|
||||
", keyword='" + keyword + '\'' +
|
||||
", parameters=" + parameters +
|
||||
", outputDefs=" + outputDefs +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", condition=" + condition +
|
||||
", validator=" + validator +
|
||||
", loopEnable=" + loopEnable +
|
||||
", loopIntervalMs=" + loopIntervalMs +
|
||||
", loopBreakCondition=" + loopBreakCondition +
|
||||
", maxLoopCount=" + maxLoopCount +
|
||||
", retryEnable=" + retryEnable +
|
||||
", resetRetryCountAfterNormal=" + resetRetryCountAfterNormal +
|
||||
", maxRetryCount=" + maxRetryCount +
|
||||
", retryIntervalMs=" + retryIntervalMs +
|
||||
", computeCostExpr='" + computeCostExpr + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class StartNode extends BaseNode {
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
return chain.getState().resolveParameters(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.node;
|
||||
|
||||
import com.jfinal.template.Engine;
|
||||
import com.jfinal.template.Template;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.util.Maps;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TemplateNode extends BaseNode {
|
||||
|
||||
private static final Engine engine;
|
||||
private String template;
|
||||
|
||||
static {
|
||||
engine = Engine.create("template", e -> {
|
||||
e.addSharedStaticMethod(StringUtil.class);
|
||||
});
|
||||
}
|
||||
|
||||
public String getTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
public void setTemplate(String template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> parameters = chain.getState().resolveParameters(this);
|
||||
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
|
||||
Template templateByString = engine.getTemplateByString(template);
|
||||
templateByString.render(parameters, result);
|
||||
|
||||
String outputDef = "output";
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
if (outputDefs != null && !outputDefs.isEmpty()) {
|
||||
String parameterName = outputDefs.get(0).getName();
|
||||
if (StringUtil.hasText(parameterName)) outputDef = parameterName;
|
||||
}
|
||||
|
||||
return Maps.of(outputDef, result.toString());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TemplateNode{" +
|
||||
"template='" + template + '\'' +
|
||||
", parameters=" + parameters +
|
||||
", outputDefs=" + outputDefs +
|
||||
", id='" + id + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", condition=" + condition +
|
||||
", validator=" + validator +
|
||||
", loopEnable=" + loopEnable +
|
||||
", loopIntervalMs=" + loopIntervalMs +
|
||||
", loopBreakCondition=" + loopBreakCondition +
|
||||
", maxLoopCount=" + maxLoopCount +
|
||||
", retryEnable=" + retryEnable +
|
||||
", resetRetryCountAfterNormal=" + resetRetryCountAfterNormal +
|
||||
", maxRetryCount=" + maxRetryCount +
|
||||
", retryIntervalMs=" + retryIntervalMs +
|
||||
", computeCostExpr='" + computeCostExpr + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser;
|
||||
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.DataType;
|
||||
import com.easyagents.flow.core.chain.JsCodeCondition;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.chain.RefType;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public abstract class BaseNodeParser<T extends BaseNode> implements NodeParser<T> {
|
||||
|
||||
private static final JSONObject EMPTY_JSON_OBJECT = new JSONObject(Collections.emptyMap());
|
||||
|
||||
private ChainParser chainParser;
|
||||
|
||||
@Override
|
||||
public ChainParser getChainParser() {
|
||||
return chainParser;
|
||||
}
|
||||
|
||||
public JSONObject getData(JSONObject nodeObject) {
|
||||
JSONObject jsonObject = nodeObject.getJSONObject("data");
|
||||
return jsonObject != null ? jsonObject : EMPTY_JSON_OBJECT;
|
||||
}
|
||||
|
||||
public void addParameters(BaseNode node, JSONObject data) {
|
||||
List<Parameter> inputParameters = getParameters(data, "parameters");
|
||||
node.setParameters(inputParameters);
|
||||
}
|
||||
|
||||
public List<Parameter> getParameters(JSONObject data, String key) {
|
||||
return getParameters(data.getJSONArray(key));
|
||||
}
|
||||
|
||||
public List<Parameter> getParameters(JSONArray parametersJsonArray) {
|
||||
if (parametersJsonArray == null || parametersJsonArray.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Parameter> parameters = new ArrayList<>(parametersJsonArray.size());
|
||||
for (int i = 0; i < parametersJsonArray.size(); i++) {
|
||||
JSONObject parameterJsonObject = parametersJsonArray.getJSONObject(i);
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setId(parameterJsonObject.getString("id"));
|
||||
parameter.setName(parameterJsonObject.getString("name"));
|
||||
parameter.setDescription(parameterJsonObject.getString("description"));
|
||||
parameter.setDataType(DataType.ofValue(parameterJsonObject.getString("dataType")));
|
||||
parameter.setRef(parameterJsonObject.getString("ref"));
|
||||
parameter.setValue(parameterJsonObject.getString("value"));
|
||||
parameter.setDefaultValue(parameterJsonObject.getString("defaultValue"));
|
||||
parameter.setRefType(RefType.ofValue(parameterJsonObject.getString("refType")));
|
||||
parameter.setDataType(DataType.ofValue(parameterJsonObject.getString("dataType")));
|
||||
parameter.setRequired(parameterJsonObject.getBooleanValue("required"));
|
||||
parameter.setDefaultValue(parameterJsonObject.getString("defaultValue"));
|
||||
|
||||
//新增
|
||||
parameter.setContentType(parameterJsonObject.getString("contentType"));
|
||||
parameter.setEnums(parameterJsonObject.getJSONArray("enums"));
|
||||
parameter.setFormType(parameterJsonObject.getString("formType"));
|
||||
parameter.setFormLabel(parameterJsonObject.getString("formLabel"));
|
||||
parameter.setFormPlaceholder(parameterJsonObject.getString("formPlaceholder"));
|
||||
parameter.setFormAttrs(parameterJsonObject.getString("formAttrs"));
|
||||
parameter.setFormDescription(parameterJsonObject.getString("formDescription"));
|
||||
|
||||
JSONArray children = parameterJsonObject.getJSONArray("children");
|
||||
if (children != null && !children.isEmpty()) {
|
||||
parameter.addChildren(getParameters(children));
|
||||
}
|
||||
|
||||
parameters.add(parameter);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
|
||||
public void addOutputDefs(BaseNode node, JSONObject data) {
|
||||
List<Parameter> outputDefs = getParameters(data, "outputDefs");
|
||||
if (outputDefs == null || outputDefs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
node.setOutputDefs(outputDefs);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public T parse(JSONObject nodeJSONObject, JSONObject chainJSONObject, ChainParser chainParser) {
|
||||
this.chainParser = chainParser;
|
||||
JSONObject data = getData(nodeJSONObject);
|
||||
T node = doParse(nodeJSONObject, data, chainJSONObject);
|
||||
if (node != null) {
|
||||
|
||||
node.setId(nodeJSONObject.getString("id"));
|
||||
node.setParentId(nodeJSONObject.getString("parentId"));
|
||||
node.setName(nodeJSONObject.getString("label"));
|
||||
node.setDescription(nodeJSONObject.getString("description"));
|
||||
|
||||
if (!data.isEmpty()) {
|
||||
|
||||
addParameters(node, data);
|
||||
addOutputDefs(node, data);
|
||||
|
||||
String conditionString = data.getString("condition");
|
||||
|
||||
if (StringUtil.hasText(conditionString)) {
|
||||
node.setCondition(new JsCodeCondition(conditionString.trim()));
|
||||
}
|
||||
|
||||
// Boolean async = data.getBoolean("async");
|
||||
// if (async != null) {
|
||||
// node.setAsync(async);
|
||||
// }
|
||||
|
||||
String name = data.getString("title");
|
||||
if (StringUtil.hasText(name)) {
|
||||
node.setName(name);
|
||||
}
|
||||
|
||||
String description = data.getString("description");
|
||||
if (StringUtil.hasText(description)) {
|
||||
node.setDescription(description);
|
||||
}
|
||||
|
||||
// 循环执行 start =======
|
||||
Boolean loopEnable = data.getBoolean("loopEnable");
|
||||
if (loopEnable != null) {
|
||||
node.setLoopEnable(loopEnable);
|
||||
}
|
||||
|
||||
if (loopEnable != null && loopEnable) {
|
||||
Long loopIntervalMs = data.getLong("loopIntervalMs");
|
||||
if (loopIntervalMs == null) {
|
||||
loopIntervalMs = 3000L;
|
||||
}
|
||||
node.setLoopIntervalMs(loopIntervalMs);
|
||||
|
||||
Integer maxLoopCount = data.getInteger("maxLoopCount");
|
||||
if (maxLoopCount != null) {
|
||||
node.setMaxLoopCount(maxLoopCount);
|
||||
}
|
||||
|
||||
String loopBreakCondition = data.getString("loopBreakCondition");
|
||||
if (StringUtil.hasText(loopBreakCondition)) {
|
||||
node.setLoopBreakCondition(new JsCodeCondition(loopBreakCondition.trim()));
|
||||
}
|
||||
}
|
||||
// 循环执行 end =======
|
||||
|
||||
|
||||
// 错误重试 start =======
|
||||
Boolean retryEnable = data.getBoolean("retryEnable");
|
||||
if (retryEnable != null) {
|
||||
node.setRetryEnable(retryEnable);
|
||||
}
|
||||
|
||||
if (retryEnable != null && retryEnable) {
|
||||
Boolean resetRetryCountAfterNormal = data.getBoolean("resetRetryCountAfterNormal");
|
||||
if (resetRetryCountAfterNormal == null) {
|
||||
resetRetryCountAfterNormal = true;
|
||||
}
|
||||
node.setResetRetryCountAfterNormal(resetRetryCountAfterNormal);
|
||||
|
||||
Long retryIntervalMs = data.getLong("retryIntervalMs");
|
||||
if (retryIntervalMs == null) {
|
||||
retryIntervalMs = 1000L;
|
||||
}
|
||||
node.setRetryIntervalMs(retryIntervalMs);
|
||||
|
||||
Integer maxRetryCount = data.getInteger("maxRetryCount");
|
||||
if (maxRetryCount == null) {
|
||||
maxRetryCount = 3;
|
||||
}
|
||||
node.setMaxRetryCount(maxRetryCount);
|
||||
}
|
||||
// 错误重试 end =======
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
protected abstract T doParse(JSONObject nodeJSONObject, JSONObject data, JSONObject chainJSONObject);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Edge;
|
||||
import com.easyagents.flow.core.chain.JsCodeCondition;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.util.CollectionUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChainParser {
|
||||
|
||||
private final Map<String, NodeParser<?>> nodeParserMap;
|
||||
|
||||
public ChainParser(Map<String, NodeParser<?>> nodeParserMap) {
|
||||
this.nodeParserMap = nodeParserMap;
|
||||
}
|
||||
|
||||
public Map<String, NodeParser<?>> getNodeParserMap() {
|
||||
return nodeParserMap;
|
||||
}
|
||||
|
||||
public void addNodeParser(String type, NodeParser<?> nodeParser) {
|
||||
this.nodeParserMap.put(type, nodeParser);
|
||||
}
|
||||
|
||||
public void removeNodeParser(String type) {
|
||||
this.nodeParserMap.remove(type);
|
||||
}
|
||||
|
||||
|
||||
public ChainDefinition parse(String jsonString) {
|
||||
if (StringUtil.noText(jsonString)) {
|
||||
throw new IllegalStateException("jsonString is empty");
|
||||
}
|
||||
|
||||
JSONObject root = JSON.parseObject(jsonString);
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
JSONArray edges = root.getJSONArray("edges");
|
||||
|
||||
return parse(root, nodes, edges);
|
||||
}
|
||||
|
||||
|
||||
public ChainDefinition parse(JSONObject chainJSONObject, JSONArray nodes, JSONArray edges) {
|
||||
if (CollectionUtil.noItems(nodes) || CollectionUtil.noItems(edges)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject nodeObject = nodes.getJSONObject(i);
|
||||
// if ((parentNode == null && StringUtil.noText(nodeObject.getString("parentId")))
|
||||
// || (parentNode != null && parentNode.getString("id").equals(nodeObject.getString("parentId")))) {
|
||||
Node node = parseNode(chainJSONObject, nodeObject);
|
||||
if (node != null) {
|
||||
definition.addNode(node);
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
for (int i = 0; i < edges.size(); i++) {
|
||||
// JSONObject edgeObject = edges.getJSONObject(i);
|
||||
// JSONObject edgeData = edgeObject.getJSONObject("data");
|
||||
// if ((parentNode == null && (edgeData == null || StringUtil.noText(edgeData.getString("parentNodeId"))))
|
||||
// || (parentNode != null && edgeData != null && edgeData.getString("parentNodeId").equals(parentNode.getString("id"))
|
||||
// //不添加子流程里的第一条 edge(也就是父节点连接子节点的第一条线)
|
||||
// && !parentNode.getString("id").equals(edgeObject.getString("source")))) {
|
||||
// ChainEdge edge = parseEdge(edgeObject);
|
||||
// if (edge != null) {
|
||||
// chain.addEdge(edge);
|
||||
// }
|
||||
// }
|
||||
|
||||
JSONObject edgeObject = edges.getJSONObject(i);
|
||||
Edge edge = parseEdge(edgeObject);
|
||||
if (edge == null) {
|
||||
continue;
|
||||
}
|
||||
// if (parentNode == null ||
|
||||
// //不添加子流程里的第一条 edge(也就是父节点连接子节点的第一条线)
|
||||
// (!parentNode.getString("id").equals(edgeObject.getString("source")))
|
||||
// ) {
|
||||
definition.addEdge(edge);
|
||||
// }
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
private Node parseNode(JSONObject chainJSONObject, JSONObject nodeObject) {
|
||||
String type = nodeObject.getString("type");
|
||||
if (StringUtil.noText(type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
NodeParser<?> nodeParser = nodeParserMap.get(type);
|
||||
return nodeParser == null ? null : nodeParser.parse(nodeObject, chainJSONObject, this);
|
||||
}
|
||||
|
||||
|
||||
private Edge parseEdge(JSONObject edgeObject) {
|
||||
if (edgeObject == null) return null;
|
||||
Edge edge = new Edge();
|
||||
edge.setId(edgeObject.getString("id"));
|
||||
edge.setSource(edgeObject.getString("source"));
|
||||
edge.setTarget(edgeObject.getString("target"));
|
||||
|
||||
JSONObject data = edgeObject.getJSONObject("data");
|
||||
if (data == null || data.isEmpty()) {
|
||||
return edge;
|
||||
}
|
||||
|
||||
String conditionString = data.getString("condition");
|
||||
if (StringUtil.hasText(conditionString)) {
|
||||
edge.setCondition(new JsCodeCondition(conditionString.trim()));
|
||||
}
|
||||
return edge;
|
||||
}
|
||||
|
||||
public void addAllParsers(Map<String, NodeParser<?>> defaultNodeParsers) {
|
||||
this.nodeParserMap.putAll(defaultNodeParsers);
|
||||
}
|
||||
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
|
||||
public static final class Builder {
|
||||
|
||||
private final Map<String, NodeParser<?>> customParsers = new HashMap<>();
|
||||
private boolean includeDefaults = true;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否包含默认节点解析器(默认为 true)。
|
||||
*/
|
||||
public Builder withDefaultParsers(boolean include) {
|
||||
this.includeDefaults = include;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义节点解析器(会覆盖同名的默认解析器)。
|
||||
*/
|
||||
public Builder addParser(String type, NodeParser<?> parser) {
|
||||
if (type == null || parser == null) {
|
||||
throw new IllegalArgumentException("type and parser must not be null");
|
||||
}
|
||||
this.customParsers.put(type, parser);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加自定义解析器。
|
||||
*/
|
||||
public Builder addParsers(Map<String, NodeParser<?>> parsers) {
|
||||
if (parsers != null) {
|
||||
this.customParsers.putAll(parsers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChainParser build() {
|
||||
Map<String, NodeParser<?>> finalMap = new HashMap<>();
|
||||
|
||||
if (includeDefaults) {
|
||||
finalMap.putAll(DefaultNodeParsers.getDefaultNodeParsers());
|
||||
}
|
||||
|
||||
// 自定义解析器覆盖默认
|
||||
finalMap.putAll(customParsers);
|
||||
|
||||
return new ChainParser(finalMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser;
|
||||
|
||||
import com.easyagents.flow.core.parser.impl.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class DefaultNodeParsers {
|
||||
|
||||
private static final Map<String, NodeParser<?>> defaultNodeParsers = new HashMap<>();
|
||||
|
||||
static {
|
||||
defaultNodeParsers.put("startNode", new StartNodeParser());
|
||||
defaultNodeParsers.put("codeNode", new CodeNodeParser());
|
||||
defaultNodeParsers.put("confirmNode", new ConfirmNodeParser());
|
||||
|
||||
defaultNodeParsers.put("httpNode", new HttpNodeParser());
|
||||
defaultNodeParsers.put("knowledgeNode", new KnowledgeNodeParser());
|
||||
defaultNodeParsers.put("loopNode", new LoopNodeParser());
|
||||
defaultNodeParsers.put("searchEngineNode", new SearchEngineNodeParser());
|
||||
defaultNodeParsers.put("templateNode", new TemplateNodeParser());
|
||||
|
||||
defaultNodeParsers.put("endNode", new EndNodeParser());
|
||||
defaultNodeParsers.put("llmNode", new LlmNodeParser());
|
||||
}
|
||||
|
||||
public static Map<String, NodeParser<?>> getDefaultNodeParsers() {
|
||||
return defaultNodeParsers;
|
||||
}
|
||||
|
||||
public static void registerDefaultNodeParser(String type, NodeParser<?> nodeParser) {
|
||||
defaultNodeParsers.put(type, nodeParser);
|
||||
}
|
||||
|
||||
public static void unregisterDefaultNodeParser(String type) {
|
||||
defaultNodeParsers.remove(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
|
||||
public interface NodeParser<T extends Node> {
|
||||
|
||||
ChainParser getChainParser();
|
||||
|
||||
T parse(JSONObject nodeJSONObject, JSONObject chainJSONObject, ChainParser chainParser);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
public class CodeNodeParser extends BaseNodeParser<CodeNode> {
|
||||
|
||||
@Override
|
||||
public CodeNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
String engine = data.getString("engine");
|
||||
CodeNode codeNode = new CodeNode();
|
||||
codeNode.setEngine(engine);
|
||||
codeNode.setCode(data.getString("code"));
|
||||
return codeNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.node.ConfirmNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConfirmNodeParser extends BaseNodeParser<ConfirmNode> {
|
||||
|
||||
@Override
|
||||
public ConfirmNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
|
||||
ConfirmNode confirmNode = new ConfirmNode();
|
||||
confirmNode.setMessage(data.getString("message"));
|
||||
|
||||
List<Parameter> confirms = getParameters(data, "confirms");
|
||||
if (confirms != null && !confirms.isEmpty()) {
|
||||
confirmNode.setConfirms(confirms);
|
||||
}
|
||||
|
||||
return confirmNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
public class EndNodeParser extends BaseNodeParser<EndNode> {
|
||||
|
||||
@Override
|
||||
public EndNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
EndNode endNode = new EndNode();
|
||||
endNode.setMessage(data.getString("message"));
|
||||
return endNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.node.HttpNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HttpNodeParser extends BaseNodeParser<HttpNode> {
|
||||
|
||||
@Override
|
||||
public HttpNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
HttpNode httpNode = new HttpNode();
|
||||
httpNode.setUrl(data.getString("url"));
|
||||
httpNode.setMethod(data.getString("method"));
|
||||
httpNode.setBodyJson(data.getString("bodyJson"));
|
||||
httpNode.setRawBody(data.getString("rawBody"));
|
||||
httpNode.setBodyType(data.getString("bodyType"));
|
||||
// httpNode.setFileStorage(tinyflow.getFileStorage());
|
||||
|
||||
List<Parameter> headers = getParameters(data, "headers");
|
||||
httpNode.setHeaders(headers);
|
||||
|
||||
List<Parameter> formData = getParameters(data, "formData");
|
||||
httpNode.setFormData(formData);
|
||||
|
||||
List<Parameter> formUrlencoded = getParameters(data, "formUrlencoded");
|
||||
httpNode.setFormUrlencoded(formUrlencoded);
|
||||
|
||||
return httpNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.KnowledgeNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
public class KnowledgeNodeParser extends BaseNodeParser<KnowledgeNode> {
|
||||
|
||||
@Override
|
||||
public KnowledgeNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
KnowledgeNode knowledgeNode = new KnowledgeNode();
|
||||
knowledgeNode.setKnowledgeId(data.get("knowledgeId"));
|
||||
knowledgeNode.setLimit(data.getString("limit"));
|
||||
knowledgeNode.setKeyword(data.getString("keyword"));
|
||||
|
||||
return knowledgeNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.llm.Llm;
|
||||
import com.easyagents.flow.core.node.LlmNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LlmNodeParser extends BaseNodeParser<LlmNode> {
|
||||
|
||||
@Override
|
||||
public LlmNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
LlmNode llmNode = new LlmNode();
|
||||
llmNode.setLlmId(data.getString("llmId"));
|
||||
llmNode.setUserPrompt(data.getString("userPrompt"));
|
||||
llmNode.setSystemPrompt(data.getString("systemPrompt"));
|
||||
llmNode.setOutType(data.getString("outType"));
|
||||
|
||||
|
||||
Llm.ChatOptions chatOptions = new Llm.ChatOptions();
|
||||
chatOptions.setTopK(data.containsKey("topK") ? data.getInteger("topK") : 10);
|
||||
chatOptions.setTopP(data.containsKey("topP") ? data.getFloat("topP") : 0.8F);
|
||||
chatOptions.setTemperature(data.containsKey("temperature") ? data.getFloat("temperature") : 0.8F);
|
||||
llmNode.setChatOptions(chatOptions);
|
||||
|
||||
// LlmProvider llmProvider = tinyflow.getLlmProvider();
|
||||
// if (llmProvider != null) {
|
||||
// Llm llm = llmProvider.getLlm(data.getString("llmId"));
|
||||
// llmNode.setLlm(llm);
|
||||
// }
|
||||
|
||||
// 支持图片识别输入
|
||||
List<Parameter> images = getParameters(data, "images");
|
||||
llmNode.setImages(images);
|
||||
|
||||
return llmNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.node.LoopNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LoopNodeParser extends BaseNodeParser<LoopNode> {
|
||||
|
||||
@Override
|
||||
public LoopNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
LoopNode loopNode = new LoopNode();
|
||||
|
||||
// 这里需要设置 id,先设置 id 后, loopNode.setLoopChain(chain); 才能取获取当前节点的 id
|
||||
// loopNode.setId(root.getString("id"));
|
||||
|
||||
List<Parameter> loopVars = getParameters(data, "loopVars");
|
||||
if (!loopVars.isEmpty()) {
|
||||
loopNode.setLoopVar(loopVars.get(0));
|
||||
}
|
||||
|
||||
// JSONArray nodes = chainJSONObject.getJSONArray("nodes");
|
||||
// JSONArray edges = chainJSONObject.getJSONArray("edges");
|
||||
|
||||
// ChainDefinition chain = getChainParser().parse(chainJSONObject, nodes, edges, root);
|
||||
// loopNode.setLoopChain(chain);
|
||||
|
||||
return loopNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.SearchEngineNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
public class SearchEngineNodeParser extends BaseNodeParser<SearchEngineNode> {
|
||||
|
||||
@Override
|
||||
public SearchEngineNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
SearchEngineNode searchEngineNode = new SearchEngineNode();
|
||||
searchEngineNode.setKeyword(data.getString("keyword"));
|
||||
searchEngineNode.setLimit(data.getString("limit"));
|
||||
|
||||
String engine = data.getString("engine");
|
||||
searchEngineNode.setEngine(engine);
|
||||
|
||||
// if (tinyflow.getSearchEngineProvider() != null) {
|
||||
// SearchEngine searchEngine = tinyflow.getSearchEngineProvider().getSearchEngine(engine);
|
||||
// searchEngineNode.setSearchEngine(searchEngine);
|
||||
// }
|
||||
|
||||
return searchEngineNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
public class StartNodeParser extends BaseNodeParser<StartNode> {
|
||||
|
||||
@Override
|
||||
public StartNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
return new StartNode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.parser.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.TemplateNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
public class TemplateNodeParser extends BaseNodeParser<TemplateNode> {
|
||||
|
||||
@Override
|
||||
public TemplateNode doParse(JSONObject root, JSONObject data, JSONObject chainJSONObject) {
|
||||
TemplateNode templateNode = new TemplateNode();
|
||||
templateNode.setTemplate(data.getString("template"));
|
||||
return templateNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.searchengine;
|
||||
|
||||
|
||||
public abstract class BaseSearchEngine implements SearchEngine {
|
||||
|
||||
protected String apiUrl;
|
||||
protected String apiKey;
|
||||
protected String keyword;
|
||||
protected String searchCount;
|
||||
protected String otherProperties;
|
||||
|
||||
public String getApiUrl() {
|
||||
return apiUrl;
|
||||
}
|
||||
|
||||
public void setApiUrl(String apiUrl) {
|
||||
this.apiUrl = apiUrl;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
public void setApiKey(String apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public String getSearchCount() {
|
||||
return searchCount;
|
||||
}
|
||||
|
||||
public void setSearchCount(String searchCount) {
|
||||
this.searchCount = searchCount;
|
||||
}
|
||||
|
||||
public String getOtherProperties() {
|
||||
return otherProperties;
|
||||
}
|
||||
|
||||
public void setOtherProperties(String otherProperties) {
|
||||
this.otherProperties = otherProperties;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.searchengine;
|
||||
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.SearchEngineNode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface SearchEngine {
|
||||
|
||||
List<Map<String, Object>> search(String keyword, int limit, SearchEngineNode searchEngineNode, Chain chain);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026, Michael Yang 杨福海 (fuhai999@gmail.com).
|
||||
* <p>
|
||||
* Licensed under the GNU Lesser General Public License (LGPL) ,Version 3.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
* <p>
|
||||
* 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 com.easyagents.flow.core.searchengine;
|
||||
|
||||
import com.easyagents.flow.core.searchengine.impl.BochaaiSearchEngineImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchEngineManager {
|
||||
|
||||
public List<SearchEngineProvider> providers = new ArrayList<>();
|
||||
|
||||
private static class ManagerHolder {
|
||||
private static final SearchEngineManager INSTANCE = new SearchEngineManager();
|
||||
}
|
||||
|
||||
private SearchEngineManager() {
|
||||
BochaaiSearchEngineImpl bochaaiSearchEngine = new BochaaiSearchEngineImpl();
|
||||
providers.add(id -> {
|
||||
if ("bocha".equals(id) || "bochaai".equals(id)) {
|
||||
return bochaaiSearchEngine;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static SearchEngineManager getInstance() {
|
||||
return ManagerHolder.INSTANCE;
|
||||
}
|
||||
|
||||
public void registerProvider(SearchEngineProvider provider) {
|
||||
providers.add(provider);
|
||||
}
|
||||
|
||||
public void removeProvider(SearchEngineProvider provider) {
|
||||
providers.remove(provider);
|
||||
}
|
||||
|
||||
public SearchEngine geSearchEngine(Object searchEngineId) {
|
||||
for (SearchEngineProvider provider : providers) {
|
||||
SearchEngine searchEngine = provider.getSearchEngine(searchEngineId);
|
||||
if (searchEngine != null) {
|
||||
return searchEngine;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user