feat(react-agent): add session factory infrastructure, memory storage config, and integration tests

- Add MemoryStorageConfig/MemoryStorageMode and per-backend configs (Redis, MySQL, workspace file)
- Add AgentSessionFactoryRegistry with NONE, JVM, WORKSPACE_FILE, REDIS, MYSQL implementations
- Add integration test suite with EL-orchestrated Spring Boot tests
- Remove per-module READMEs in favor of unified guide
- Update POMs, CLAUDE.md, AGENTS.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
everywhere.z
2026-04-29 19:12:29 +08:00
parent 0820f45032
commit 61824d6956
42 changed files with 1368 additions and 301 deletions

3
.gitignore vendored
View File

@@ -24,6 +24,7 @@
*.del
*.pmd
.tern-project
*.factorypath
# Logs and databases #
@@ -52,8 +53,10 @@ Thumbs.db
# Build output directies
/target
*/target
**/target
/build
*/build
**/build
# IntelliJ specific files/directories

237
AGENTS.md Normal file
View File

@@ -0,0 +1,237 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Overview
LiteFlow (v2.15.3) is a lightweight rules engine framework for complex component-based business orchestration. It uses a DSL to drive workflows with support for hot reload and 11 scripting languages. The project targets Java 8+ (up to JDK 25) and has 2000+ test cases.
**Official Documentation**: https://liteflow.cc/pages/5816c5/
## Commands
### Build
```bash
# Build entire project (uses 'compile' profile by default, includes test modules)
mvn clean package -DskipTests
# Build with tests
mvn clean package
# Build specific module
mvn clean package -DskipTests -pl liteflow-core
# Build for release (production modules only, excludes tests)
mvn clean package -DskipTests -P release
```
### Run Tests
```bash
# Run all tests
mvn test
# Run tests for specific module (30+ test modules in liteflow-testcase-el/)
mvn test -pl liteflow-testcase-el/liteflow-testcase-el-springboot
# Run single test class
mvn test -pl liteflow-core -Dtest=FlowExecutorTest
# Run specific test method
mvn test -pl liteflow-core -Dtest=FlowExecutorTest#testExecute
```
### Other Commands
```bash
# Dependency tree
mvn dependency:tree
# View module structure
ls liteflow-*/pom.xml
```
## High-Level Architecture
### Core Execution Model
**FlowExecutor****Chain****Condition Tree****Node Components**
1. **FlowExecutor**: Entry point for executing workflows (`execute2Resp(chainId, param)`)
2. **FlowBus**: Central metadata registry for all chains and nodes (thread-safe, supports hot reload)
3. **Chain**: Named workflow composed of an EL expression that compiles to a Condition tree
4. **Slot/DataBus**: Thread-safe context management using slot pooling (configurable `slotSize`)
5. **NodeComponent**: Base class for all business logic components
### Key Architectural Patterns
#### 1. Two-Stage Parsing
Chains are built in two phases to handle circular dependencies:
- **Phase 1**: Register chain IDs (creates placeholder chains)
- **Phase 2**: Build complete condition trees with EL parsing
#### 2. EL Expression Language
Uses QLExpress to parse declarative workflows. Example operators:
- **THEN(a, b, c)**: Sequential execution
- **WHEN(a, b, c)**: Parallel execution (async)
- **IF(condition, trueNode, falseNode)**: Conditional branching
- **SWITCH(selector).to(a, b, c)**: Multi-way branching
- **FOR(count).DO(loop)**: Fixed-count loop
- **WHILE(condition).DO(loop)**: Condition-based loop
- **ITERATOR(iterator).DO(loop)**: Iterator-based loop
- **RETRY(node).times(3).forException(Ex.class)**: Retry mechanism
- **CATCH(node).DO(handler)**: Exception handling
- **TIMEOUT(node).time(1000)**: Execution timeout (ms)
- **PRE(a, b)**: Pre-conditions (always run before chain, even on error)
- **FINALLY(a, b)**: Finally-conditions (always run after chain)
- **AND(a, b)**, **OR(a, b)**, **NOT(a)**: Boolean logic for IF conditions
- **node.tag("t")**, **.data("k","v")**, **.id("id")**: Per-node modifiers
Rules are defined in XML/JSON/YML:
```xml
<chain name="myChain">
THEN(a, WHEN(b, c).maxWaitSeconds(5), IF(e, f, g));
</chain>
```
#### 3. Component Types
All extend `NodeComponent` but have specialized behaviors:
- **NodeComponent**: Standard synchronous component (`process()` method)
- **NodeBooleanComponent**: Returns boolean for IF conditions (`processBoolean()`)
- **NodeSwitchComponent**: Returns string for SWITCH routing (`processSwitch()`)
- **NodeIteratorComponent**: Provides iteration logic for ITERATOR construct
- **NodeForComponent**: Controls FOR loop behavior
- **ScriptComponent**: Script-based components (Groovy, JS, Python, etc.)
**Declarative Component Pattern**: Any Spring bean can become a component without extending base classes by using `@LiteflowCmpDefine` (class-level, specifies `NodeTypeEnum`) and `@LiteflowMethod` (method-level, maps to `LiteFlowMethodEnum`). This avoids class hierarchy constraints.
**Component Lifecycle Hooks** (override in NodeComponent subclasses or via `@LiteflowMethod`):
- `isAccess()` gate check before execution; return `false` to skip
- `beforeProcess()` / `afterProcess()` pre/post hooks per component
- `onSuccess()` / `onError()` outcome callbacks
- `isContinueOnError()` whether WHEN continues if this node fails
- `isEnd()` signals chain should stop after this node
- `rollback()` called in reverse order on failure
**`@FallbackCmp`**: Annotate a fallback component that activates when the primary component is not found or throws.
#### 4. Slot-Based Context
Thread-safe execution context:
- `DataBus.offerSlot(chainId)` acquires a slot from pool
- Slot contains execution metadata, context beans, and step tracking
- `DataBus.releaseSlot(slotIndex)` returns slot to pool
- Pool size configurable via `slotSize` property
#### 5. Parse Mode Strategies
Three modes (`ParseModeEnum`):
- **PARSE_ALL_ON_START**: Parse all chains at startup (default)
- **PARSE_ONE_ON_FIRST_EXEC**: Lazy parse each chain on first use (faster startup)
- **PARSE_ALL_ON_FIRST_EXEC**: Parse all chains on first any chain execution
### Module Structure
#### Core Modules
- **liteflow-core**: Core engine (FlowExecutor, FlowBus, DataBus, Slot, Condition system, component model)
- **liteflow-el-builder**: Programmatic chain builder API using QLExpress
#### Rule Source Plugins (6 implementations in `liteflow-rule-plugin/`)
- **liteflow-rule-zk**: ZooKeeper configuration source
- **liteflow-rule-sql**: SQL database configuration source
- **liteflow-rule-nacos**: Nacos configuration center
- **liteflow-rule-etcd**: etcd configuration source
- **liteflow-rule-apollo**: Apollo configuration center
- **liteflow-rule-redis**: Redis configuration source
#### Script Plugins (11 languages in `liteflow-script-plugin/`)
- **liteflow-script-groovy**: Groovy scripting
- **liteflow-script-javascript**: Rhino JavaScript (JSR223)
- **liteflow-script-graaljs**: GraalVM JavaScript
- **liteflow-script-qlexpress**: Alibaba QLExpress
- **liteflow-script-python**: Jython (Python on JVM)
- **liteflow-script-lua**: LuaJ
- **liteflow-script-aviator**: Aviator expression language
- **liteflow-script-java**: Janino (Java compiler)
- **liteflow-script-javax**: JSR223 standard Java compiler
- **liteflow-script-javax-pro**: Liquor-based Java compiler (enhanced)
- **liteflow-script-kotlin**: Kotlin scripting
#### Framework Integration
- **liteflow-spring**: Spring framework integration (component scanning, bean lifecycle, AOP)
- **liteflow-spring-boot-starter**: Spring Boot auto-configuration with `@ConfigurationProperties`
- **liteflow-solon-plugin**: Solon framework integration (lightweight alternative to Spring)
#### Testing Infrastructure
30+ test modules in `liteflow-testcase-el/` organized by:
1. **Framework**: springboot, springnative, solon, nospring
2. **Config Sources**: zk, nacos, etcd, apollo, redis, sql
3. **Scripts**: One module per language + multi-language scenarios
4. **Features**: builder, declare, routechain, monitoring, timeout, etc.
Test pattern:
```java
@SpringBootTest
@TestPropertySource(value = "classpath:/application.properties")
public class MyTest {
@Resource private FlowExecutor flowExecutor;
@Test
public void test() {
LiteflowResponse response = flowExecutor.execute2Resp("chainId", "arg");
Assertions.assertTrue(response.isSuccess());
}
}
```
### Important Design Patterns
#### SPI Pattern for Extensibility
Used extensively for:
- `ContextAware`: Framework abstraction (Spring vs non-Spring)
- `PathContentParser`: Custom file path resolution
- `CmpAroundAspect`: Global component lifecycle hooks
- `DeclComponentParser`: Declarative component parsing
- Script executors for each language
#### Lifecycle Hooks
Multiple extension points:
- `PostProcessChainBuildLifeCycle`: Before/after chain building
- `PostProcessNodeBuildLifeCycle`: Before/after node building
- `PostProcessChainExecuteLifeCycle`: Before/after chain execution
- `PostProcessFlowExecuteLifeCycle`: Before/after flow execution
#### Rollback Mechanism
Components implement `rollback()` for automatic rollback on failure (executed in reverse order via `executeSteps` deque).
#### Hot Reload Support
- `MonitorFile` watches rule files for changes
- `reloadRule()` refreshes without restart
- Copy-on-write collections (unless `fastLoad=true`) prevent concurrent modification
#### Metadata Caching
- `FlowBus` uses CopyOnWriteHashMap (or regular HashMap with `fastLoad`)
- EL MD5 caching for expression reuse
- Chain caching configurable (`chainCacheEnabled`, `chainCacheCapacity`)
### Critical Configuration (`LiteflowConfig`)
- **ruleSource**: Rule file locations (supports XML, JSON, YML)
- **parseMode**: Parsing strategy (affects startup performance)
- **slotSize**: Context slot pool size
- **enableMonitorFile**: Hot reload of rule files
- **supportMultipleType**: Mix XML/JSON/YML rules
- **whenMaxWaitTime**: Timeout for WHEN parallel execution
- **fastLoad**: Disable CopyOnWrite for faster startup
- **enableVirtualThread**: Use virtual threads (JDK 21+)
### Key File Locations
- Core engine: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/`
- FlowExecutor: `liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java`
- FlowBus: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/FlowBus.java`
- Component base: `liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java`
- Condition system: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/condition/`
- EL parser: `liteflow-core/src/main/java/com/yomahub/liteflow/parser/el/`
### Important Conventions
- **Naming**: Components identified by `nodeId`, chains by `chainId`
- **Thread Safety**: Extensive ThreadLocal and concurrent collections
- **Fail-Fast**: Validation at parse time with detailed error messages
- **Fluent APIs**: Builder pattern for chain construction (EL builder)
- **Namespaces**: Chains can be organized into namespaces
- **Versioning**: Uses `${revision}` placeholder (currently 2.15.3) via flatten-maven-plugin

View File

@@ -71,12 +71,19 @@ Chains are built in two phases to handle circular dependencies:
#### 2. EL Expression Language
Uses QLExpress to parse declarative workflows. Example operators:
- **THEN(a, b, c)**: Sequential execution
- **WHEN(a, b, c)**: Parallel execution
- **IF(condition, THEN(x), ELSE(y))**: Conditional branching
- **WHEN(a, b, c)**: Parallel execution (async)
- **IF(condition, trueNode, falseNode)**: Conditional branching
- **SWITCH(selector).to(a, b, c)**: Multi-way branching
- **FOR(count).DO(loop)**: Loop execution
- **RETRY(node).times(3).forException()**: Retry mechanism
- **FOR(count).DO(loop)**: Fixed-count loop
- **WHILE(condition).DO(loop)**: Condition-based loop
- **ITERATOR(iterator).DO(loop)**: Iterator-based loop
- **RETRY(node).times(3).forException(Ex.class)**: Retry mechanism
- **CATCH(node).DO(handler)**: Exception handling
- **TIMEOUT(node).time(1000)**: Execution timeout (ms)
- **PRE(a, b)**: Pre-conditions (always run before chain, even on error)
- **FINALLY(a, b)**: Finally-conditions (always run after chain)
- **AND(a, b)**, **OR(a, b)**, **NOT(a)**: Boolean logic for IF conditions
- **node.tag("t")**, **.data("k","v")**, **.id("id")**: Per-node modifiers
Rules are defined in XML/JSON/YML:
```xml
@@ -94,6 +101,18 @@ All extend `NodeComponent` but have specialized behaviors:
- **NodeForComponent**: Controls FOR loop behavior
- **ScriptComponent**: Script-based components (Groovy, JS, Python, etc.)
**Declarative Component Pattern**: Any Spring bean can become a component without extending base classes by using `@LiteflowCmpDefine` (class-level, specifies `NodeTypeEnum`) and `@LiteflowMethod` (method-level, maps to `LiteFlowMethodEnum`). This avoids class hierarchy constraints.
**Component Lifecycle Hooks** (override in NodeComponent subclasses or via `@LiteflowMethod`):
- `isAccess()` gate check before execution; return `false` to skip
- `beforeProcess()` / `afterProcess()` pre/post hooks per component
- `onSuccess()` / `onError()` outcome callbacks
- `isContinueOnError()` whether WHEN continues if this node fails
- `isEnd()` signals chain should stop after this node
- `rollback()` called in reverse order on failure
**`@FallbackCmp`**: Annotate a fallback component that activates when the primary component is not found or throws.
#### 4. Slot-Based Context
Thread-safe execution context:
- `DataBus.offerSlot(chainId)` acquires a slot from pool

View File

@@ -90,7 +90,7 @@ class ModelSpecTest {
/** 仅用于测试的最小 ModelSpec 子类。 */
static class TestSpec extends ModelSpec<TestSpec> {
@Override protected Model resolve(AgentConfig cfg) { return null; }
@Override public Model resolve(AgentConfig cfg) { return null; }
}
@Test
@@ -192,11 +192,16 @@ public abstract class ModelSpec<SELF extends ModelSpec<SELF>> {
* 把本描述符解析为 agentscope {@link Model} 实例。
* 实现需从 {@link AgentConfig} 中读取对应平台的 credential
* 并把共性 + 个性参数翻译成 agentscope 的 GenerateOptions。
* <p>
* 本方法是框架 SPI{@code ReActAgentComponent} 在不同包中调用,
* 因此必须为 {@code public}subclass override 同样需要 {@code public})。
*/
protected abstract Model resolve(AgentConfig cfg);
public abstract Model resolve(AgentConfig cfg);
}
```
> **重要**`resolve` 必须是 `public`,因为 Task 8 中 `ReActAgentComponent`(在 `com.yomahub.liteflow.agent.component` 包)会调用 `model(ctx).resolve(agentConfig())`。Java 不允许 override 缩小可见性,所以后续 Tasks 3-7 中所有 spec 子类的 `resolve` override 也必须是 `public`,测试中的 `TestSpec.resolve` 同理。
- [ ] **Step 4: 再次运行测试,确认通过**
```bash
@@ -513,7 +518,7 @@ public class OpenAISpec extends ModelSpec<OpenAISpec> {
public Double getPresencePenalty() { return presencePenalty; }
@Override
protected Model resolve(AgentConfig cfg) {
public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getOpenai(), "liteflow.agent.openai");
return buildModel(cred.getApiKey(), cred.getBaseUrl());
@@ -740,7 +745,7 @@ public class OpenAICompatibleSpec extends OpenAISpec {
}
@Override
protected Model resolve(AgentConfig cfg) {
public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireCompatible(
cfg.getOpenaiCompatible(), configKey, "liteflow.agent.openai-compatible");
String baseUrl = (cred.getBaseUrl() != null && !cred.getBaseUrl().isBlank())
@@ -1018,7 +1023,7 @@ public class AnthropicSpec extends ModelSpec<AnthropicSpec> {
public Boolean getThinkingEnabled() { return thinkingEnabled; }
@Override
protected Model resolve(AgentConfig cfg) {
public Model resolve(AgentConfig cfg) {
PlatformCredential cred;
if (compatibleConfigKey == null) {
cred = CredentialResolver.requireFirstClass(
@@ -1239,7 +1244,7 @@ public class GeminiSpec extends ModelSpec<GeminiSpec> {
public Integer getThinkingBudget() { return thinkingBudget; }
@Override
protected Model resolve(AgentConfig cfg) {
public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getGemini(), "liteflow.agent.gemini");
@@ -1429,7 +1434,7 @@ public class DashScopeSpec extends ModelSpec<DashScopeSpec> {
public Integer getThinkingBudget() { return thinkingBudget; }
@Override
protected Model resolve(AgentConfig cfg) {
public Model resolve(AgentConfig cfg) {
PlatformCredential cred = CredentialResolver.requireFirstClass(
cfg.getDashscope(), "liteflow.agent.dashscope");

View File

@@ -13,6 +13,7 @@ public class AgentConfig {
private PlatformCredential gemini = new PlatformCredential();
private PlatformCredential dashscope = new PlatformCredential();
private Map<String, PlatformCredential> openaiCompatible = new LinkedHashMap<>();
private Map<String, PlatformCredential> anthropicCompatible = new LinkedHashMap<>();
public WorkspaceConfig getWorkspace() { return workspace; }
public void setWorkspace(WorkspaceConfig v) { this.workspace = v; }
@@ -32,4 +33,6 @@ public class AgentConfig {
public void setDashscope(PlatformCredential v) { this.dashscope = v; }
public Map<String, PlatformCredential> getOpenaiCompatible() { return openaiCompatible; }
public void setOpenaiCompatible(Map<String, PlatformCredential> v) { this.openaiCompatible = v; }
public Map<String, PlatformCredential> getAnthropicCompatible() { return anthropicCompatible; }
public void setAnthropicCompatible(Map<String, PlatformCredential> v) { this.anthropicCompatible = v; }
}

View File

@@ -0,0 +1,40 @@
package com.yomahub.liteflow.property.agent;
/**
* Memory persistence settings for ReActAgent sessions.
*
* <p>This config is intentionally orthogonal to {@link SessionConfig} (which
* controls JVM-side session caching, idle timeout, LRU eviction). Memory
* storage decides <em>where</em> the agent's conversation history is durably
* kept; session config decides <em>how long</em> a hot agent is held in memory.
*/
public class MemoryStorageConfig {
/** Default is {@link MemoryStorageMode#JVM} so existing deployments behave unchanged. */
private MemoryStorageMode mode = MemoryStorageMode.JVM;
private WorkspaceMemoryConfig workspace = new WorkspaceMemoryConfig();
private RedisMemoryConfig redis = new RedisMemoryConfig();
private MysqlMemoryConfig mysql = new MysqlMemoryConfig();
/** Whether to lazily load existing session state on first {@code process()}. */
private boolean loadOnFirstUse = true;
/** Whether to save session state after a successful {@code process()}. */
private boolean saveAfterCall = true;
/** Whether to save session state when {@code process()} throws. */
private boolean saveOnError = true;
public MemoryStorageMode getMode() { return mode; }
public void setMode(MemoryStorageMode mode) { this.mode = mode; }
public WorkspaceMemoryConfig getWorkspace() { return workspace; }
public void setWorkspace(WorkspaceMemoryConfig workspace) { this.workspace = workspace; }
public RedisMemoryConfig getRedis() { return redis; }
public void setRedis(RedisMemoryConfig redis) { this.redis = redis; }
public MysqlMemoryConfig getMysql() { return mysql; }
public void setMysql(MysqlMemoryConfig mysql) { this.mysql = mysql; }
public boolean isLoadOnFirstUse() { return loadOnFirstUse; }
public void setLoadOnFirstUse(boolean loadOnFirstUse) { this.loadOnFirstUse = loadOnFirstUse; }
public boolean isSaveAfterCall() { return saveAfterCall; }
public void setSaveAfterCall(boolean saveAfterCall) { this.saveAfterCall = saveAfterCall; }
public boolean isSaveOnError() { return saveOnError; }
public void setSaveOnError(boolean saveOnError) { this.saveOnError = saveOnError; }
}

View File

@@ -0,0 +1,24 @@
package com.yomahub.liteflow.property.agent;
/**
* Storage backend used to persist a ReActAgent's memory across executions
* within a session.
*
* <ul>
* <li>{@link #NONE} do not persist or even hold memory; equivalent to a stateless agent</li>
* <li>{@link #JVM} keep memory in JVM heap only (default; behaviour identical to pre-2.15.4 releases)</li>
* <li>{@link #WORKSPACE_FILE} persist memory as JSON files under each session's workspace directory
* using AgentScope's JsonSession</li>
* <li>{@link #REDIS} persist memory through AgentScope's RedisSession; requires the user to
* provide a {@code RedissonClient} / {@code UnifiedJedis} / {@code RedisClient} bean</li>
* <li>{@link #MYSQL} persist memory through AgentScope's MysqlSession; requires the user to
* provide a {@code javax.sql.DataSource} bean</li>
* </ul>
*/
public enum MemoryStorageMode {
NONE,
JVM,
WORKSPACE_FILE,
REDIS,
MYSQL
}

View File

@@ -0,0 +1,30 @@
package com.yomahub.liteflow.property.agent;
/**
* Settings that only apply when {@link MemoryStorageMode#MYSQL} is selected.
*
* <p>The DataSource is supplied by the user via {@code beanName} and looked up
* through ContextAware. LiteFlow never builds a JDBC connection pool itself.
*/
public class MysqlMemoryConfig {
/** Bean name of the {@link javax.sql.DataSource} to look up via ContextAware. */
private String dataSourceBeanName;
/** Database name passed to {@code MysqlSession}. Empty means use AgentScope's default ({@code agentscope}). */
private String databaseName;
/** Table name passed to {@code MysqlSession}. Empty means use AgentScope's default ({@code agentscope_sessions}). */
private String tableName;
/** When true, AgentScope auto-creates database & table; defaults to false to respect production constraints. */
private boolean createIfNotExist = false;
public String getDataSourceBeanName() { return dataSourceBeanName; }
public void setDataSourceBeanName(String dataSourceBeanName) { this.dataSourceBeanName = dataSourceBeanName; }
public String getDatabaseName() { return databaseName; }
public void setDatabaseName(String databaseName) { this.databaseName = databaseName; }
public String getTableName() { return tableName; }
public void setTableName(String tableName) { this.tableName = tableName; }
public boolean isCreateIfNotExist() { return createIfNotExist; }
public void setCreateIfNotExist(boolean createIfNotExist) { this.createIfNotExist = createIfNotExist; }
}

View File

@@ -0,0 +1,28 @@
package com.yomahub.liteflow.property.agent;
/**
* Settings that only apply when {@link MemoryStorageMode#REDIS} is selected.
*
* <p>Connections are not created by LiteFlow. Users must provide a Redis client
* bean (Redisson / Jedis / Lettuce) through the framework's {@code ContextAware}
* (Spring bean lookup, Solon container, etc.).
*/
public class RedisMemoryConfig {
/** Bean name of the Redis client to look up via ContextAware. */
private String beanName;
/** Type of the configured client; affects how AgentScope adapts it. */
private RedisClientType clientType = RedisClientType.REDISSON;
/** Key prefix used inside Redis. */
private String keyPrefix = "liteflow:agent:session";
public String getBeanName() { return beanName; }
public void setBeanName(String beanName) { this.beanName = beanName; }
public RedisClientType getClientType() { return clientType; }
public void setClientType(RedisClientType clientType) { this.clientType = clientType; }
public String getKeyPrefix() { return keyPrefix; }
public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; }
public enum RedisClientType { REDISSON, JEDIS, LETTUCE }
}

View File

@@ -6,6 +6,7 @@ public class SessionConfig {
private Duration idleTimeout = Duration.ofMinutes(30);
private Duration cleanupInterval = Duration.ofMinutes(1);
private int maxSessions = 10_000;
private MemoryStorageConfig memory = new MemoryStorageConfig();
public Duration getIdleTimeout() { return idleTimeout; }
public void setIdleTimeout(Duration v) { this.idleTimeout = v; }
@@ -13,4 +14,6 @@ public class SessionConfig {
public void setCleanupInterval(Duration v) { this.cleanupInterval = v; }
public int getMaxSessions() { return maxSessions; }
public void setMaxSessions(int v) { this.maxSessions = v; }
public MemoryStorageConfig getMemory() { return memory; }
public void setMemory(MemoryStorageConfig memory) { this.memory = memory; }
}

View File

@@ -0,0 +1,14 @@
package com.yomahub.liteflow.property.agent;
/**
* Settings that only apply when {@link MemoryStorageMode#WORKSPACE_FILE} is selected.
*
* <p>The persistence sub-directory is hard-coded to {@code .agent-session} so it
* stays out of the way of tool-produced files in the per-session workspace and
* is not configurable on purpose; users who want a custom location can plug in
* their own {@code AgentSessionFactory} via SPI.
*/
public class WorkspaceMemoryConfig {
/** Fixed sub-directory created under the workspace root that holds session JSON files. */
public static final String SUB_DIR = ".agent-session";
}

View File

@@ -15,6 +15,8 @@ class AgentConfigTest {
assertNotNull(c.getDefaults());
assertNotNull(c.getOpenaiCompatible());
assertTrue(c.getOpenaiCompatible().isEmpty());
assertNotNull(c.getAnthropicCompatible());
assertTrue(c.getAnthropicCompatible().isEmpty());
WorkspaceConfig w = c.getWorkspace();
assertNull(w.getRoot());
@@ -43,8 +45,12 @@ class AgentConfigTest {
void platform_credentials_are_independent_instances() {
AgentConfig c = new AgentConfig();
c.getOpenai().setApiKey("k1");
c.getOpenaiCompatible().put("compatible-openai", new PlatformCredential());
c.getAnthropicCompatible().put("compatible-anthropic", new PlatformCredential());
assertNull(c.getAnthropic().getApiKey());
assertNull(c.getGemini().getApiKey());
assertNull(c.getDashscope().getApiKey());
assertFalse(c.getOpenaiCompatible().containsKey("compatible-anthropic"));
assertFalse(c.getAnthropicCompatible().containsKey("compatible-openai"));
}
}

View File

@@ -1,9 +0,0 @@
# liteflow-react-agent-anthropic
Anthropic Claude 模型支持。
## 使用
```java
AnthropicChatModel model = AnthropicModelFactory.of(apiKey, "claude-sonnet-4-6");
```

View File

@@ -14,8 +14,8 @@
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
@@ -34,23 +34,5 @@
<artifactId>anthropic-java</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -11,4 +11,12 @@ public final class AnthropicModelFactory {
.modelName(modelName)
.build();
}
}
public static AnthropicChatModel custom(String apiKey, String baseUrl, String modelName) {
return AnthropicChatModel.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.modelName(modelName)
.build();
}
}

View File

@@ -1,73 +0,0 @@
# liteflow-react-agent-core
## 快速上手
### 1. 添加依赖(选择至少一个平台模块)
```xml
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-react-agent-openai</artifactId>
<version>${liteflow.version}</version>
</dependency>
```
### 2. 配置
```yaml
liteflow:
agent:
workspace:
root: /var/lib/liteflow/agent-workspaces
shell:
mode: whitelist
whitelist: [ls, cat, grep]
openai-compatible:
deepseek:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com/v1
```
### 3. 定义 Agent
```java
@LiteflowComponent("reviewAgent")
public class ReviewAgent extends ReActAgentComponent {
@Override protected Model buildModel(ReActAgentContext ctx) {
return OpenAICompatiblePresets.deepseek(
agentConfig().getOpenaiCompatible().get("deepseek").getApiKey(),
"deepseek-chat"
);
}
@Override protected String systemPrompt(ReActAgentContext ctx) { return "你是审核专家"; }
@Override protected String userPrompt(ReActAgentContext ctx) {
return ctx.getSlot().getRequestData(String.class);
}
}
```
### 4. EL 编排
```xml
<chain name="reviewChain">
THEN(prepare, reviewAgent, notify);
</chain>
```
## 核心概念
- **Session**:由 `resolveSessionId` 决定;默认 `slot.getRequestId()`(一次性)。覆写后可复用 memory 与 workspace 实现多轮对话。
- **Workspace**:每 session 一个目录,在 `liteflow.agent.workspace.root` 之下。内置 `WorkspaceFileTools` 强制路径围栏。
- **Shell**`ManagedShellCommandTool``liteflow.agent.shell.mode` 做 whitelist/blacklist/disabled 校验,首 token 不在策略内即拒绝。
## 可选覆写
| 方法 | 默认行为 | 说明 |
|------|----------|------|
| `tools(ctx)` | 空列表 | 注册自定义 agentscope @Tool 对象 |
| `resolveSessionId(slot)` | `slot.getRequestId()` | 覆写以实现多轮对话 |
| `maxIterations()` | 配置文件默认值 (15) | ReAct 最大推理轮数 |
| `enableShellTool()` | true | 是否启用受管 shell 工具 |
| `enableWorkspaceFileTools()` | true | 是否启用受管文件工具 |
| `hooks(ctx)` | 空列表 | agentscope Hook 列表 |
| `handleReply(reply, ctx)` | 写入 slot.responseData | 自定义回复处理 |

View File

@@ -16,8 +16,8 @@
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
@@ -43,36 +43,5 @@
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-console-standalone</artifactId>
<version>1.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,4 +1,4 @@
/**
* LiteFlow ReAct Agent core module.
* LiteFlow ReAct Agent 核心模块。
*/
package com.yomahub.liteflow.agent;

View File

@@ -1,7 +1,13 @@
package com.yomahub.liteflow.agent.session;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.agent.session.factory.AgentSessionFactoryRegistry;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.session.Session;
import io.agentscope.core.session.SessionManager;
import java.io.IOException;
import java.net.URLEncoder;
@@ -18,6 +24,19 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* 跟踪当前 JVM 中存活的 AgentSession并桥接到可插拔的
* {@link Session}(由 {@link AgentSessionFactoryRegistry} 提供)。
*
* <p>这里将两类职责保持独立:
* <ul>
* <li>JVM 内缓存、加锁和 LRU 淘汰(本类负责)</li>
* <li>持久化存储(委托给 AgentScope 的 Session 抽象)</li>
* </ul>
*
* <p>淘汰(空闲或超过容量)只移除缓存的 agent 实例。
* workspace 文件以及磁盘、Redis、MySQL 中的持久化 session 数据会被保留。
*/
public class AgentSessionManager implements AutoCloseable {
private static final Pattern SAFE = Pattern.compile("[a-zA-Z0-9_\\-]+");
@@ -26,6 +45,8 @@ public class AgentSessionManager implements AutoCloseable {
private final Path root;
private final Map<String, AgentSession> sessions = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleaner;
/** memory 模式为 NONE 时可能为 null。 */
private final Session storage;
public AgentSessionManager(AgentConfig config) {
this.config = config;
@@ -42,6 +63,7 @@ public class AgentSessionManager implements AutoCloseable {
} else if (!Files.isDirectory(root)) {
throw new AgentConfigException("workspace root does not exist: " + root);
}
this.storage = AgentSessionFactoryRegistry.createSession(config);
long every = Math.max(20, config.getSession().getCleanupInterval().toMillis());
this.cleaner = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "liteflow-agent-session-cleaner");
@@ -68,6 +90,35 @@ public class AgentSessionManager implements AutoCloseable {
return sessions.containsKey(safeId(sessionId));
}
/**
* 将之前持久化的状态懒加载恢复到 agent 中。
* 同一个 session id 在当前 JVM 生命周期内应只调用一次,并且应在 agent
* 构建完成后、首次 {@code agent.call(...)} 前调用。
*/
public void loadIfExists(AgentSession session, ReActAgent agent) {
if (storage == null || agent == null) return;
MemoryStorageConfig mc = config.getSession().getMemory();
if (!mc.isLoadOnFirstUse()) return;
if (mc.getMode() == MemoryStorageMode.NONE) return;
SessionManager.forSessionId(session.getSessionId())
.withSession(storage)
.addComponent(agent)
.loadIfExists();
}
/** 持久化 agent 当前状态。失败会向调用方暴露。 */
public void save(AgentSession session, ReActAgent agent) {
if (storage == null || agent == null) return;
MemoryStorageConfig mc = config.getSession().getMemory();
if (mc.getMode() == MemoryStorageMode.NONE) return;
SessionManager.forSessionId(session.getSessionId())
.withSession(storage)
.addComponent(agent)
.saveSession();
}
public Session storage() { return storage; }
static String safeId(String raw) {
if (raw == null || raw.isEmpty()) return "_";
if (SAFE.matcher(raw).matches()) return raw;
@@ -79,7 +130,8 @@ public class AgentSessionManager implements AutoCloseable {
while (sessions.size() > max) {
sessions.values().stream()
.min(Comparator.comparing(AgentSession::getLastActive))
.ifPresent(victim -> remove(victim, true));
// LRU 淘汰只移除 JVM 内缓存;持久化数据保持不变。
.ifPresent(victim -> evictFromCache(victim, false));
}
}
@@ -89,14 +141,20 @@ public class AgentSessionManager implements AutoCloseable {
if (s.getLastActive().isAfter(cutoff)) continue;
if (!s.getLock().tryLock()) continue;
try {
remove(s, config.getWorkspace().isCleanupOnSessionExpire());
evictFromCache(s, config.getWorkspace().isCleanupOnSessionExpire());
} finally {
s.getLock().unlock();
}
}
}
private void remove(AgentSession s, boolean cleanWorkspace) {
/**
* @param cleanWorkspace 为 true 时,同时删除磁盘上的 workspace 目录
* (保留历史行为)。存储在其他位置的持久化 session
* 状态(例如 workspaceRoot/.agent-session、Redis、
* MySQL不会在这里被删除。
*/
private void evictFromCache(AgentSession s, boolean cleanWorkspace) {
sessions.remove(s.getSessionId(), s);
if (cleanWorkspace) {
deleteRecursively(s.getWorkspaceDir());
@@ -118,6 +176,9 @@ public class AgentSessionManager implements AutoCloseable {
if (config.getWorkspace().isCleanupOnJvmShutdown()) {
sessions.values().forEach(s -> deleteRecursively(s.getWorkspaceDir()));
}
if (storage != null) {
try { storage.close(); } catch (Exception ignored) {}
}
sessions.clear();
}
}

View File

@@ -0,0 +1,30 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import io.agentscope.core.session.Session;
/**
* 用于为 {@link io.agentscope.core.session.Session} 接入额外持久化后端的 SPI。
*
* <p>框架内置模式({@code JVM}、{@code WORKSPACE_FILE}、{@code REDIS}、
* {@code MYSQL}、{@code NONE})。需要其他后端(例如 PostgreSQL、OSS、
* 加密 JSON的用户可以在 {@code META-INF/services/}{@link AgentSessionFactory}
* 下注册自定义工厂。
*/
public interface AgentSessionFactory {
/**
* 当前工厂处理的模式。所有已注册工厂之间必须唯一。
*/
MemoryStorageMode mode();
/**
* 根据 agent 配置构建底层 {@link Session}。该方法会在首次
* {@code process()} 时懒调用,而不是在框架启动时调用。
*
* @return 非 null 的 Session如果需要跳过持久化则返回 {@code null}
* {@link MemoryStorageMode#NONE} 对应的工厂会返回 {@code null})。
*/
Session create(AgentConfig agentConfig);
}

View File

@@ -0,0 +1,54 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import io.agentscope.core.session.Session;
import java.util.EnumMap;
import java.util.Map;
import java.util.ServiceLoader;
/**
* 根据指定模式解析合适的 {@link AgentSessionFactory}。
*
* <p>解析顺序:
* <ol>
* <li>通过 {@link ServiceLoader} 注册的外部工厂</li>
* <li>框架内置工厂JVM、workspace、Redis、MySQL、none</li>
* </ol>
* 出现冲突时外部工厂优先,因此用户可以覆盖内置实现
* (例如用自定义加密 JSON 工厂替换默认 workspace 工厂)。
*/
public final class AgentSessionFactoryRegistry {
private static final Map<MemoryStorageMode, AgentSessionFactory> FACTORIES = new EnumMap<>(MemoryStorageMode.class);
static {
// 先注册内置实现;如果存在 SPI 实现,再由 SPI 覆盖。
register(new InMemoryAgentSessionFactory());
register(new WorkspaceAgentSessionFactory());
register(new RedisAgentSessionFactory());
register(new MysqlAgentSessionFactory());
register(new NoneAgentSessionFactory());
for (AgentSessionFactory f : ServiceLoader.load(AgentSessionFactory.class)) {
register(f);
}
}
private AgentSessionFactoryRegistry() { }
private static void register(AgentSessionFactory f) {
FACTORIES.put(f.mode(), f);
}
/** 根据配置模式构建 Session。{@link MemoryStorageMode#NONE} 可能返回 {@code null}。 */
public static Session createSession(AgentConfig cfg) {
MemoryStorageMode mode = cfg.getSession().getMemory().getMode();
AgentSessionFactory f = FACTORIES.get(mode);
if (f == null) {
throw new AgentConfigException("No AgentSessionFactory registered for mode: " + mode);
}
return f.create(cfg);
}
}

View File

@@ -0,0 +1,26 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import io.agentscope.core.session.InMemorySession;
import io.agentscope.core.session.Session;
/**
* 使用 AgentScope 的内存存储支持 {@link MemoryStorageMode#JVM} 模式。
*
* <p>注意:状态仍会在同一个 JVM 内跨调用保留(适合希望在单进程内保留多轮记忆的场景),
* 但进程退出后会丢失。如果需要跨重启持久化,请选择
* {@code WORKSPACE_FILE}、{@code REDIS} 或 {@code MYSQL}。
*/
public class InMemoryAgentSessionFactory implements AgentSessionFactory {
@Override
public MemoryStorageMode mode() {
return MemoryStorageMode.JVM;
}
@Override
public Session create(AgentConfig agentConfig) {
return new InMemorySession();
}
}

View File

@@ -0,0 +1,52 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import com.yomahub.liteflow.property.agent.MysqlMemoryConfig;
import com.yomahub.liteflow.spi.holder.ContextAwareHolder;
import io.agentscope.core.session.Session;
import io.agentscope.core.session.mysql.MysqlSession;
import javax.sql.DataSource;
/**
* 支持 {@link MemoryStorageMode#MYSQL} 模式。使用用户提供的
* {@link DataSource} beanLiteFlow 不自行创建 JDBC 连接池。
*/
public class MysqlAgentSessionFactory implements AgentSessionFactory {
@Override
public MemoryStorageMode mode() {
return MemoryStorageMode.MYSQL;
}
@Override
public Session create(AgentConfig cfg) {
MysqlMemoryConfig mc = cfg.getSession().getMemory().getMysql();
if (mc.getDataSourceBeanName() == null || mc.getDataSourceBeanName().trim().isEmpty()) {
throw new AgentConfigException(
"liteflow.agent.session.memory.mysql.dataSourceBeanName is required when mode=MYSQL");
}
Object bean = ContextAwareHolder.loadContextAware().getBean(mc.getDataSourceBeanName());
if (bean == null) {
throw new AgentConfigException("DataSource bean not found: " + mc.getDataSourceBeanName());
}
if (!(bean instanceof DataSource)) {
throw new AgentConfigException("Bean '" + mc.getDataSourceBeanName() + "' is not a DataSource; got "
+ bean.getClass().getName());
}
DataSource ds = (DataSource) bean;
String db = mc.getDatabaseName();
String table = mc.getTableName();
boolean hasCustom = (db != null && !db.isEmpty()) || (table != null && !table.isEmpty());
try {
if (hasCustom) {
return new MysqlSession(ds, db, table, mc.isCreateIfNotExist());
}
return new MysqlSession(ds, mc.isCreateIfNotExist());
} catch (Exception e) {
throw new AgentConfigException("Failed to build MysqlSession", e);
}
}
}

View File

@@ -0,0 +1,22 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import io.agentscope.core.session.Session;
/**
* 返回 {@code null} Session用于通知 AgentSessionManager 跳过所有加载和保存操作。
* 供 {@link MemoryStorageMode#NONE} 使用。
*/
public class NoneAgentSessionFactory implements AgentSessionFactory {
@Override
public MemoryStorageMode mode() {
return MemoryStorageMode.NONE;
}
@Override
public Session create(AgentConfig agentConfig) {
return null;
}
}

View File

@@ -0,0 +1,79 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import com.yomahub.liteflow.property.agent.RedisMemoryConfig;
import com.yomahub.liteflow.spi.holder.ContextAwareHolder;
import io.agentscope.core.session.Session;
/**
* 通过把用户提供的 Redis 客户端 beanRedisson、Jedis、Lettuce
* 适配到 AgentScope 的 RedisSession 来支持 {@link MemoryStorageMode#REDIS}。
*
* <p>Redis 客户端类通过反射查找,因此 core 模块不会对 Redisson、Jedis、Lettuce
* 产生硬性的编译期依赖。如果选择 REDIS 模式但 classpath 中缺少匹配驱动,
* 会在首次 {@code process()} 时失败,而不是在框架启动时失败。
*/
public class RedisAgentSessionFactory implements AgentSessionFactory {
private static final String REDIS_SESSION_CLASS = "io.agentscope.core.session.redis.RedisSession";
@Override
public MemoryStorageMode mode() {
return MemoryStorageMode.REDIS;
}
@Override
public Session create(AgentConfig cfg) {
RedisMemoryConfig rc = cfg.getSession().getMemory().getRedis();
if (rc.getBeanName() == null || rc.getBeanName().trim().isEmpty()) {
throw new AgentConfigException(
"liteflow.agent.session.memory.redis.beanName is required when mode=REDIS");
}
Object client = ContextAwareHolder.loadContextAware().getBean(rc.getBeanName());
if (client == null) {
throw new AgentConfigException("Redis client bean not found: " + rc.getBeanName());
}
String builderMethod;
String clientFqn;
switch (rc.getClientType()) {
case REDISSON:
builderMethod = "redissonClient";
clientFqn = "org.redisson.api.RedissonClient";
break;
case JEDIS:
builderMethod = "jedisClient";
clientFqn = "redis.clients.jedis.UnifiedJedis";
break;
case LETTUCE:
builderMethod = "lettuceClient";
clientFqn = "io.lettuce.core.RedisClient";
break;
default:
throw new AgentConfigException("Unsupported redis client type: " + rc.getClientType());
}
try {
Class<?> sessionClass = Class.forName(REDIS_SESSION_CLASS);
Object builder = sessionClass.getMethod("builder").invoke(null);
Class<?> clientType = Class.forName(clientFqn);
if (!clientType.isInstance(client)) {
throw new AgentConfigException("Bean '" + rc.getBeanName() + "' is not a "
+ clientFqn + "; got " + client.getClass().getName());
}
builder.getClass().getMethod(builderMethod, clientType).invoke(builder, client);
if (rc.getKeyPrefix() != null && !rc.getKeyPrefix().isEmpty()) {
builder.getClass().getMethod("keyPrefix", String.class).invoke(builder, rc.getKeyPrefix());
}
return (Session) builder.getClass().getMethod("build").invoke(builder);
} catch (ClassNotFoundException e) {
throw new AgentConfigException(
"Class not found while building RedisSession: " + e.getMessage()
+ ". Add the matching driver dependency (Redisson/Jedis/Lettuce).", e);
} catch (AgentConfigException e) {
throw e;
} catch (Exception e) {
throw new AgentConfigException("Failed to build RedisSession", e);
}
}
}

View File

@@ -0,0 +1,46 @@
package com.yomahub.liteflow.agent.session.factory;
import com.yomahub.liteflow.agent.exception.AgentConfigException;
import com.yomahub.liteflow.property.agent.AgentConfig;
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
import com.yomahub.liteflow.property.agent.WorkspaceMemoryConfig;
import io.agentscope.core.session.JsonSession;
import io.agentscope.core.session.Session;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 通过把 JSON 文件存储在 {@code workspace.root/.agent-session/<sessionId>/}
* 下来支持 {@link MemoryStorageMode#WORKSPACE_FILE}。
*
* <p>session 存储子目录特意与 {@code workspace.root/<sessionId>/}
* 形式的单 session 工具 workspace 分离,避免
* {@link com.yomahub.liteflow.agent.tool.WorkspaceFileTools} 读取或覆盖
* agent 自身记忆。
*/
public class WorkspaceAgentSessionFactory implements AgentSessionFactory {
@Override
public MemoryStorageMode mode() {
return MemoryStorageMode.WORKSPACE_FILE;
}
@Override
public Session create(AgentConfig cfg) {
if (cfg.getWorkspace() == null || cfg.getWorkspace().getRoot() == null) {
throw new AgentConfigException(
"liteflow.agent.workspace.root is required when session.memory.mode=WORKSPACE_FILE");
}
Path root = Paths.get(cfg.getWorkspace().getRoot()).toAbsolutePath().normalize()
.resolve(WorkspaceMemoryConfig.SUB_DIR);
try {
Files.createDirectories(root);
} catch (IOException e) {
throw new AgentConfigException("cannot create session storage dir: " + root, e);
}
return new JsonSession(root);
}
}

View File

@@ -1,9 +0,0 @@
# liteflow-react-agent-dashscope
阿里云 DashScope / Qwen 模型支持。
## 使用
```java
DashScopeChatModel model = DashScopeModelFactory.of(apiKey, "qwen3-max");
```

View File

@@ -12,8 +12,8 @@
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
@@ -22,23 +22,5 @@
<artifactId>liteflow-react-agent-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,13 +0,0 @@
# liteflow-react-agent-gemini
Google Gemini 模型支持。
## 使用
```java
// 基础
GeminiChatModel m1 = GeminiModelFactory.of(apiKey, "gemini-3-flash-preview");
// 带 thinking level
GeminiChatModel m2 = GeminiModelFactory.of(apiKey, "gemini-3-flash-preview", "high");
```

View File

@@ -12,8 +12,8 @@
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
@@ -32,23 +32,5 @@
<artifactId>google-genai</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,25 +0,0 @@
# liteflow-react-agent-openai
OpenAI 及 OpenAI 兼容协议厂商支持。
## 使用
```java
// 标准 OpenAI
OpenAIChatModel m1 = OpenAIModelFactory.openai(apiKey, "gpt-4o-mini");
// DeepSeek
OpenAIChatModel m2 = OpenAICompatiblePresets.deepseek(apiKey, "deepseek-chat");
// Kimi (Moonshot)
OpenAIChatModel m3 = OpenAICompatiblePresets.kimi(apiKey, "moonshot-v1-8k");
// GLM (智谱)
OpenAIChatModel m4 = OpenAICompatiblePresets.glm(apiKey, "glm-4");
// MiniMax
OpenAIChatModel m5 = OpenAICompatiblePresets.minimax(apiKey, "abab6.5s-chat");
// 自定义 OpenAI 兼容端点
OpenAIChatModel m6 = OpenAIModelFactory.custom(apiKey, "https://your.own/v1", "your-model");
```

View File

@@ -14,8 +14,8 @@
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
@@ -24,23 +24,5 @@
<artifactId>liteflow-react-agent-core</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,91 @@
<?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">
<parent>
<artifactId>liteflow-testcase-el</artifactId>
<groupId>com.yomahub</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>liteflow-testcase-el-react-agent</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-react-agent-openai</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-react-agent-anthropic</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-react-agent-gemini</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-react-agent-dashscope</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>false</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -5,4 +5,7 @@ import static org.junit.jupiter.api.Assertions.*;
class AnthropicModelFactoryTest {
@Test void construct_ok() { assertNotNull(AnthropicModelFactory.of("k", "claude-sonnet-4-6")); }
}
@Test void custom_base_url_ok() {
assertNotNull(AnthropicModelFactory.custom("k", "https://anthropic-proxy.example.com", "claude-sonnet-4-6"));
}
}

View File

@@ -0,0 +1,20 @@
package com.yomahub.liteflow.test.agent;
/**
* 示例测试 API Key 工具:环境变量优先,再回退到 Spring 注入的 properties。
* 留空时调用 {@link org.junit.jupiter.api.Assumptions#assumeTrue} 跳过用例。
*/
public final class ApiKeys {
private ApiKeys() {}
public static String resolve(String envName, String configured) {
String env = System.getenv(envName);
if (env != null && !env.isBlank()) return env.trim();
return configured == null ? "" : configured.trim();
}
public static boolean isPresent(String s) {
return s != null && !s.isBlank();
}
}

View File

@@ -0,0 +1,170 @@
package com.yomahub.liteflow.test.agent;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.property.LiteflowConfig;
import com.yomahub.liteflow.test.agent.cmp.RecordReplyCmp;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;
import javax.annotation.Resource;
/**
* 走 LiteFlow EL 编排的 ReActAgent 示例测试。
* <p>
* 每个用例都通过 {@code flowExecutor.execute2Resp(chainId, ...)} 跑一条 chain
* Agent 节点是 {@link com.yomahub.liteflow.agent.component.ReActAgentComponent}
* 的具体子类apiKey 通过 {@code application.properties} 中的
* {@code liteflow.agent.<platform>.api-key} 注入。留空则用例自动 skip。
*/
@TestPropertySource(value = "classpath:/agent/application.properties")
@SpringBootTest(classes = ReActAgentELSpringbootTest.class)
@EnableAutoConfiguration
@ComponentScan({
"com.yomahub.liteflow.test.agent.cmp"
})
public class ReActAgentELSpringbootTest {
@Resource
private FlowExecutor flowExecutor;
@Resource
private LiteflowConfig liteflowConfig;
/** 真正运行链路并断言成功,回复非空。 */
private void runChainExpectingReply(String chainId, String question) {
LiteflowResponse response = flowExecutor.execute2Resp(chainId, question);
if (!response.isSuccess() && response.getCause() != null) {
response.getCause().printStackTrace();
}
Assertions.assertTrue(response.isSuccess(),
"chain failed: " + (response.getCause() == null ? "" : response.getCause().getMessage()));
Object reply = response.getSlot().getOutput(RecordReplyCmp.NODE_ID);
Assertions.assertNotNull(reply, "agent reply must be recorded");
System.out.println(">>> [" + chainId + "] reply=" + reply);
}
private String openaiKey() { return ApiKeys.resolve("OPENAI_API_KEY", liteflowConfig.getAgent().getOpenai().getApiKey()); }
private String anthropicKey() { return ApiKeys.resolve("ANTHROPIC_API_KEY", liteflowConfig.getAgent().getAnthropic().getApiKey()); }
private String geminiKey() { return ApiKeys.resolve("GEMINI_API_KEY", liteflowConfig.getAgent().getGemini().getApiKey()); }
private String dashscopeKey() { return ApiKeys.resolve("DASHSCOPE_API_KEY", liteflowConfig.getAgent().getDashscope().getApiKey()); }
private String deepseekKey() {
return ApiKeys.resolve("DEEPSEEK_API_KEY",
liteflowConfig.getAgent().getOpenaiCompatible()
.getOrDefault("deepseek", new com.yomahub.liteflow.property.agent.PlatformCredential())
.getApiKey());
}
/** 上下文:在 BeforeAll 里把环境变量回写到 LiteflowConfig避免 properties 留空时无 key 可用。 */
private void syncEnvKeys() {
if (ApiKeys.isPresent(openaiKey())) liteflowConfig.getAgent().getOpenai().setApiKey(openaiKey());
if (ApiKeys.isPresent(anthropicKey())) liteflowConfig.getAgent().getAnthropic().setApiKey(anthropicKey());
if (ApiKeys.isPresent(geminiKey())) liteflowConfig.getAgent().getGemini().setApiKey(geminiKey());
if (ApiKeys.isPresent(dashscopeKey())) liteflowConfig.getAgent().getDashscope().setApiKey(dashscopeKey());
if (ApiKeys.isPresent(deepseekKey())) {
liteflowConfig.getAgent().getOpenaiCompatible()
.computeIfAbsent("deepseek", k -> {
com.yomahub.liteflow.property.agent.PlatformCredential c =
new com.yomahub.liteflow.property.agent.PlatformCredential();
c.setBaseUrl("https://api.deepseek.com/v1");
return c;
})
.setApiKey(deepseekKey());
}
}
/* =====================================================================
* 单平台单 Agent 链路THEN(prepare, <agent>, recordReply)
* ===================================================================== */
@Test
public void testDeepSeekChain() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()),
"deepseek api-key 未配置,跳过 deepseekChain");
runChainExpectingReply("deepseekChain", "用一句话总结 ReAct 模式的核心思想。");
}
@Test
public void testOpenAIChain() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(openaiKey()),
"openai api-key 未配置,跳过 openaiChain");
runChainExpectingReply("openaiChain", "用一句话介绍 LiteFlow。");
}
@Test
public void testAnthropicChain() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(anthropicKey()),
"anthropic api-key 未配置,跳过 anthropicChain");
runChainExpectingReply("anthropicChain", "什么是规则引擎?一句话回答。");
}
@Test
public void testDashScopeChain() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(dashscopeKey()),
"dashscope api-key 未配置,跳过 dashscopeChain");
runChainExpectingReply("dashscopeChain", "用一句话介绍通义千问。");
}
@Test
public void testGeminiChain() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(geminiKey()),
"gemini api-key 未配置,跳过 geminiChain");
runChainExpectingReply("geminiChain", "用一句中文介绍 Gemini 模型。");
}
/* =====================================================================
* 自定义工具mathChain → mathAgent 注册 CalculatorTool
* ===================================================================== */
@Test
public void testMathChainWithCustomTool() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()),
"deepseek api-key 未配置,跳过 mathChainmathAgent 后端用 deepseek");
runChainExpectingReply("mathChain",
"请用 calculator 工具计算 (123 + 456) * 7 - 89并用一句话给出答案。");
}
/* =====================================================================
* IF 路由routerChain根据 isMath 选 mathAgent 或 deepseekAgent
* ===================================================================== */
@Test
public void testRouterChainPicksMathAgent() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()),
"deepseek api-key 未配置,跳过 routerChain");
runChainExpectingReply("routerChain", "12*34 等于多少?");
}
@Test
public void testRouterChainPicksChatAgent() {
syncEnvKeys();
Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()),
"deepseek api-key 未配置,跳过 routerChain");
runChainExpectingReply("routerChain", "你好,简单介绍下你自己。");
}
/* =====================================================================
* WHEN 并行parallelChain 同时跑 deepseek + dashscope谁后到 recordReply 取谁
* ===================================================================== */
@Test
public void testParallelChain() {
syncEnvKeys();
Assumptions.assumeTrue(
ApiKeys.isPresent(deepseekKey()) && ApiKeys.isPresent(dashscopeKey()),
"需要同时配置 deepseek 和 dashscope 的 api-key 才能运行 parallelChain");
runChainExpectingReply("parallelChain", "用一句话介绍 LiteFlow 的应用场景。");
}
}

View File

@@ -0,0 +1,67 @@
package com.yomahub.liteflow.test.agent.tool;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
/**
* 自定义工具示例:计算器。被 ReActAgentComponent.tools() 注册后,
* agent 在推理时会被 toolkit 自动发现并按需调用。
*/
public class CalculatorTool {
@Tool(name = "calculator", description = "Evaluate a basic arithmetic expression like '1+2*3'")
public String calc(@ToolParam(name = "expression",
description = "Arithmetic expression with +, -, *, /, and parentheses") String expression) {
try {
return String.valueOf(Eval.run(expression));
} catch (Exception e) {
return "ERROR: " + e.getMessage();
}
}
/** 极简递归下降表达式求值器(仅供示例,不保证健壮性)。 */
static final class Eval {
private final String s;
private int pos;
private Eval(String s) { this.s = s; }
static double run(String s) {
Eval e = new Eval(s.replaceAll("\\s+", ""));
double v = e.expr();
if (e.pos < e.s.length()) throw new IllegalArgumentException("Unexpected: " + e.s.substring(e.pos));
return v;
}
private double expr() {
double v = term();
while (pos < s.length() && (s.charAt(pos) == '+' || s.charAt(pos) == '-')) {
char op = s.charAt(pos++);
double r = term();
v = (op == '+') ? v + r : v - r;
}
return v;
}
private double term() {
double v = factor();
while (pos < s.length() && (s.charAt(pos) == '*' || s.charAt(pos) == '/')) {
char op = s.charAt(pos++);
double r = factor();
v = (op == '*') ? v * r : v / r;
}
return v;
}
private double factor() {
if (pos < s.length() && s.charAt(pos) == '(') {
pos++;
double v = expr();
if (pos >= s.length() || s.charAt(pos) != ')') throw new IllegalArgumentException("Missing )");
pos++;
return v;
}
int start = pos;
if (pos < s.length() && (s.charAt(pos) == '+' || s.charAt(pos) == '-')) pos++;
while (pos < s.length() && (Character.isDigit(s.charAt(pos)) || s.charAt(pos) == '.')) pos++;
if (start == pos) throw new IllegalArgumentException("Number expected at " + pos);
return Double.parseDouble(s.substring(start, pos));
}
}
}

View File

@@ -0,0 +1,26 @@
liteflow.rule-source=agent/flow.el.xml
liteflow.print-banner=false
# =============================================================================
# ReAct Agent 测试 API Key —— 用户在此填入,留空则对应链路测试被 Assumptions 跳过
# 也可通过环境变量覆盖OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY /
# DASHSCOPE_API_KEY / DEEPSEEK_API_KEY环境变量优先级更高
# =============================================================================
liteflow.agent.workspace.root=${java.io.tmpdir}/liteflow-react-agent-test
liteflow.agent.shell.mode=disabled
liteflow.agent.openai.api-key=
liteflow.agent.anthropic.api-key=
liteflow.agent.gemini.api-key=
liteflow.agent.dashscope.api-key=
liteflow.agent.openai-compatible.deepseek.api-key=
liteflow.agent.openai-compatible.deepseek.base-url=https://api.deepseek.com/v1
liteflow.agent.anthropic-compatible.gateway.api-key=
liteflow.agent.anthropic-compatible.gateway.base-url=https://anthropic-gateway.example.com
# 模型名(可按需覆盖)
test.openai.model=gpt-4o-mini
test.anthropic.model=claude-sonnet-4-5
test.gemini.model=gemini-2.5-flash
test.dashscope.model=qwen-plus
test.deepseek.model=deepseek-chat

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
<flow>
<!-- 1. 最简单的 Agent 调用链:准备 -> Agent 推理 -> 记录回复 -->
<chain name="deepseekChain">
THEN(prepare, deepseekAgent, recordReply);
</chain>
<chain name="openaiChain">
THEN(prepare, openaiAgent, recordReply);
</chain>
<chain name="anthropicChain">
THEN(prepare, anthropicAgent, recordReply);
</chain>
<chain name="dashscopeChain">
THEN(prepare, dashscopeAgent, recordReply);
</chain>
<chain name="geminiChain">
THEN(prepare, geminiAgent, recordReply);
</chain>
<!-- 2. 自定义工具示例mathAgent 注册了 CalculatorTool -->
<chain name="mathChain">
THEN(prepare, mathAgent, recordReply);
</chain>
<!-- 3. 用 IF EL 路由:算术题走 mathAgent其他走 deepseekAgent -->
<chain name="routerChain">
THEN(
prepare,
IF(isMath, mathAgent, deepseekAgent),
recordReply
);
</chain>
<!-- 4. 并行 WHEN同时跑两个不同后端的 Agent用 maxWaitSeconds 限时 -->
<chain name="parallelChain">
THEN(
prepare,
WHEN(deepseekAgent, dashscopeAgent).maxWaitSeconds(60),
recordReply
);
</chain>
</flow>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{40} - %msg%n</pattern>
</encoder>
</appender>
<logger name="io.agentscope" level="INFO"/>
<logger name="com.yomahub.liteflow" level="INFO"/>
<root level="WARN">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@@ -46,4 +46,17 @@
<module>liteflow-testcase-el-sql-springboot-sharding-jdbc</module>
<module>liteflow-testcase-el-script-javaxpro-springboot</module>
</modules>
<profiles>
<!-- ReAct Agent 测试模块依赖 JDK 21agentscope-java仅在 JDK 17+ 构建中拉入 -->
<profile>
<id>testcase-react-agent</id>
<activation>
<jdk>[17,)</jdk>
</activation>
<modules>
<module>liteflow-testcase-el-react-agent</module>
</modules>
</profile>
</profiles>
</project>

139
pom.xml
View File

@@ -39,7 +39,7 @@
</scm>
<properties>
<revision>2.15.3.2</revision>
<revision>2.16.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>8</maven.compiler.source>
@@ -82,7 +82,7 @@
<sharding-jdbc.version>4.1.1</sharding-jdbc.version>
<apache-commons-test.version>1.14.0</apache-commons-test.version>
<caffeine.version>2.9.3</caffeine.version>
<agentscope.version>1.0.9</agentscope.version>
<agentscope.version>1.0.11</agentscope.version>
<google-genai.version>1.38.0</google-genai.version>
</properties>
@@ -425,11 +425,64 @@
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!-- Release 插件统一定义release-main 和 release-react-agent 共用 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.7</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
<configuration>
<passphrase>${gpg.passphrase}</passphrase>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>oss</publishingServerId>
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<profiles>
<profile>
<id>compile</id>
<id>compile-8-to-16</id>
<modules>
<module>liteflow-core</module>
<module>liteflow-script-plugin</module>
@@ -442,12 +495,12 @@
<module>liteflow-benchmark</module>
</modules>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>[1.8,17)</jdk>
</activation>
</profile>
<profile>
<id>release</id>
<id>release-main</id>
<modules>
<module>liteflow-core</module>
<module>liteflow-script-plugin</module>
@@ -463,53 +516,46 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.4</version>
</plugin>
<!-- GPG -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.7</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
<configuration>
<passphrase>${gpg.passphrase}</passphrase>
</configuration>
</plugin>
<!-- Javadoc -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>oss</publishingServerId>
<autoPublish>true</autoPublish>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>release-react-agent</id>
<modules>
<module>liteflow-react-agent</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
@@ -517,11 +563,20 @@
<!-- ReAct Agent 模块:需要 Java 21+agentscope-java 要求JDK 版本满足时自动激活 -->
<profile>
<id>react-agent</id>
<id>compile-17+</id>
<activation>
<jdk>[21,)</jdk>
<jdk>[17,)</jdk>
</activation>
<modules>
<module>liteflow-core</module>
<module>liteflow-script-plugin</module>
<module>liteflow-rule-plugin</module>
<module>liteflow-spring-boot-starter</module>
<module>liteflow-spring</module>
<module>liteflow-solon-plugin</module>
<module>liteflow-testcase-el</module>
<module>liteflow-el-builder</module>
<module>liteflow-benchmark</module>
<module>liteflow-react-agent</module>
</modules>
</profile>