初始化

This commit is contained in:
2026-02-22 18:55:40 +08:00
commit 8392cdd861
496 changed files with 45020 additions and 0 deletions

67
easy-agents-flow/pom.xml Normal file
View 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>

View File

@@ -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;
}
}

View File

@@ -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";
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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);
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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 {
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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.&lt;init&gt;(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&trade; 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&trade; 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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}));
}
}

View File

@@ -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());
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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();
}
}

View File

@@ -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) {
}
}
}
}

View File

@@ -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();
}

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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();
}
}

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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