From 6c8a1b26365ae7c02ee3dcbfc103c009ee33445a Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Thu, 12 Mar 2026 10:41:17 +0800 Subject: [PATCH] refactor(action): support built-in actions --- .idea/misc.xml | 19 +-- .../partner/core/action/ActionCapability.java | 2 + .../slhaf/partner/core/action/ActionCore.java | 11 +- .../partner/core/action/entity/MetaAction.kt | 13 +- .../core/action/runner/LocalRunnerClient.java | 1 + .../core/action/runner/RunnerClient.java | 24 +++- .../action/runner/SandboxRunnerClient.java | 9 +- .../action/builtin/BuiltinActionRegistry.java | 76 +++++++++++ .../action/runner/LocalRunnerClientTest.java | 48 +++++++ .../builtin/BuiltinActionRegistryTest.java | 126 ++++++++++++++++++ 10 files changed, 310 insertions(+), 19 deletions(-) create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistry.java create mode 100644 Partner-Core/src/test/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistryTest.java diff --git a/.idea/misc.xml b/.idea/misc.xml index e68691e1..63114a05 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + @@ -9,14 +9,15 @@ - - - - - - - - + + + + + + + + + diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java index cd9a8d00..48d3a968 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCapability.java @@ -53,6 +53,8 @@ public interface ActionCapability { MetaActionInfo loadMetaActionInfo(@NonNull String actionKey); + void registerMetaActions(@NonNull Map metaActions); + Map listAvailableMetaActions(); boolean checkExists(String... actionKeys); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java index b9f0e817..469f53e5 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/ActionCore.java @@ -30,6 +30,7 @@ import java.util.stream.Collectors; @CapabilityCore(value = "action") @Slf4j public class ActionCore extends PartnerCore { + public static final String BUILTIN_LOCATION = "builtin"; private final Lock cacheLock = new ReentrantLock(); // 由于当前的执行器逻辑实现,平台线程池大小不得小于 2,这里规定为最小为 4 @@ -273,7 +274,12 @@ public class ActionCore extends PartnerCore { } @CapabilityMethod - public Map listAvailableActions() { + public void registerMetaActions(@NonNull Map metaActions) { + existedMetaActions.putAll(metaActions); + } + + @CapabilityMethod + public Map listAvailableMetaActions() { return existedMetaActions; } @@ -320,10 +326,11 @@ public class ActionCore extends PartnerCore { if (split.length < 2) { throw new MetaActionNotFoundException("未找到对应的行动程序,原因: 传入的 actionKey(" + actionKey + ") 存在异常"); } + MetaAction.Type type = BUILTIN_LOCATION.equals(split[0]) ? MetaAction.Type.BUILTIN : MetaAction.Type.MCP; return new MetaAction( split[1], metaActionInfo.isIo(), - MetaAction.Type.MCP, + type, split[0] ); } diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/MetaAction.kt b/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/MetaAction.kt index ca201293..48ace057 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/MetaAction.kt +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/entity/MetaAction.kt @@ -14,12 +14,14 @@ data class MetaAction( */ val io: Boolean = false, /** - * 行动程序类型,可分为 MCP、ORIGIN 两种,前者对应读取到的 MCP Tool、后者对应生成的临时行动程序 + * 行动程序类型,可分为 MCP、ORIGIN、BUILTIN 三种, + * 分别对应读取到的 MCP Tool、生成的临时行动程序、本地内置行动 */ val type: Type, /** * 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id; - * 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串 + * 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串; + * 当类型为 BUILTIN 时,该字段固定为 builtin */ val location: String, ) { @@ -67,7 +69,12 @@ data class MetaAction( /** * 适用于‘临时生成’的行动程序,在生成后根据序列化选项及执行情况,进行持久化 */ - ORIGIN + ORIGIN, + + /** + * 由本地内置注册表直接执行的行动 + */ + BUILTIN } } diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java index a057cb6b..97670ee1 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java @@ -224,6 +224,7 @@ public class LocalRunnerClient extends RunnerClient { response = switch (metaAction.getType()) { case MetaAction.Type.MCP -> doRunWithMcp(metaAction); case MetaAction.Type.ORIGIN -> doRunWithOrigin(metaAction); + case MetaAction.Type.BUILTIN -> doRunWithBuiltin(metaAction); }; } catch (Exception e) { response = new RunnerResponse(); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/RunnerClient.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/RunnerClient.java index ddc32673..8e0e5171 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/RunnerClient.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/RunnerClient.java @@ -1,8 +1,8 @@ package work.slhaf.partner.core.action.runner; import com.alibaba.fastjson2.JSONObject; -import io.modelcontextprotocol.server.McpStatelessAsyncServer; import lombok.Data; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import lombok.val; import org.jetbrains.annotations.Nullable; @@ -11,6 +11,7 @@ import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaAction.Result; import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.exception.ActionInitFailedException; +import work.slhaf.partner.module.modules.action.builtin.BuiltinActionRegistry; import java.io.IOException; import java.nio.file.Files; @@ -45,8 +46,8 @@ public abstract class RunnerClient { protected final ConcurrentHashMap existedMetaActions; protected final ExecutorService executor; - //TODO 仍可提供内部 MCP,但调用方式需要结合 AgentContext来获取,否则生命周期不合 - protected McpStatelessAsyncServer innerMcpServer; + @Setter + protected BuiltinActionRegistry builtinActionRegistry; /** * ActionCore 将注入虚拟线程池 @@ -82,6 +83,23 @@ public abstract class RunnerClient { public abstract void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData); + protected RunnerResponse doRunWithBuiltin(MetaAction metaAction) { + RunnerResponse response = new RunnerResponse(); + if (builtinActionRegistry == null) { + response.setOk(false); + response.setData("BuiltinActionRegistry 未初始化"); + return response; + } + try { + response.setData(builtinActionRegistry.call(metaAction.getKey(), metaAction.getParams())); + response.setOk(true); + } catch (Exception e) { + response.setOk(false); + response.setData(e.getLocalizedMessage()); + } + return response; + } + protected void createPath(String pathStr) { val path = Path.of(pathStr); try { diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java b/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java index 7bc34fcf..5231f012 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java @@ -30,8 +30,13 @@ public class SandboxRunnerClient extends RunnerClient { } protected RunnerResponse doRun(MetaAction metaAction) { - // 调用沙盒执行器 - return null; + return switch (metaAction.getType()) { + case BUILTIN -> doRunWithBuiltin(metaAction); + case MCP, ORIGIN -> { + // 调用沙盒执行器 + yield null; + } + }; } @Override diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistry.java b/Partner-Core/src/main/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistry.java new file mode 100644 index 00000000..de52d4ce --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistry.java @@ -0,0 +1,76 @@ +package work.slhaf.partner.module.modules.action.builtin; + +import com.alibaba.fastjson2.JSONObject; +import lombok.Getter; +import lombok.NonNull; +import work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability; +import work.slhaf.partner.api.agent.factory.component.abstracts.AbstractAgentModule; +import work.slhaf.partner.api.agent.factory.component.annotation.Init; +import work.slhaf.partner.core.action.ActionCapability; +import work.slhaf.partner.core.action.entity.MetaActionInfo; +import work.slhaf.partner.core.action.exception.MetaActionNotFoundException; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static work.slhaf.partner.core.action.ActionCore.BUILTIN_LOCATION; + +public class BuiltinActionRegistry extends AbstractAgentModule.Standalone { + + @Getter + private final Map definitions = new LinkedHashMap<>(); + @InjectCapability + private ActionCapability actionCapability; + + public static BuiltinActionDefinition definition(String name, MetaActionInfo metaActionInfo, + Function, Object> invoker) { + return new BuiltinActionDefinition(BUILTIN_LOCATION + "::" + name, metaActionInfo, invoker); + } + + @Init + public void init() { + definitions.clear(); + for (BuiltinActionDefinition definition : buildDefinitions()) { + definitions.put(definition.actionKey(), definition); + } + actionCapability.registerMetaActions(exportMetaActionInfos()); + actionCapability.runnerClient().setBuiltinActionRegistry(this); + } + + protected List buildDefinitions() { + return List.of(); + } + + public String call(@NonNull String actionKey, @NonNull Map params) { + BuiltinActionDefinition definition = definitions.get(actionKey); + if (definition == null) { + throw new MetaActionNotFoundException("未找到对应的内置行动程序: " + actionKey); + } + Object result = definition.invoker().apply(params); + if (result == null) { + return "null"; + } + if (result instanceof String string) { + return string; + } + if (result instanceof Number || result instanceof Boolean || result instanceof Map || result instanceof Iterable) { + return JSONObject.toJSONString(result); + } + return String.valueOf(result); + } + + private Map exportMetaActionInfos() { + Map metaActions = new LinkedHashMap<>(); + definitions.forEach((key, value) -> metaActions.put(key, value.metaActionInfo())); + return metaActions; + } + + public record BuiltinActionDefinition( + String actionKey, + MetaActionInfo metaActionInfo, + Function, Object> invoker + ) { + } +} diff --git a/Partner-Core/src/test/java/work/slhaf/partner/core/action/runner/LocalRunnerClientTest.java b/Partner-Core/src/test/java/work/slhaf/partner/core/action/runner/LocalRunnerClientTest.java index 7a31496c..c236dc5e 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/core/action/runner/LocalRunnerClientTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/core/action/runner/LocalRunnerClientTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaActionInfo; +import work.slhaf.partner.module.modules.action.builtin.BuiltinActionRegistry; import java.io.IOException; import java.nio.file.Files; @@ -843,6 +844,53 @@ public class LocalRunnerClientTest { } } + @Test + void testDoRunWithBuiltin(@TempDir Path tempDir) { + ConcurrentHashMap existedMetaActions = new ConcurrentHashMap<>(); + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + LocalRunnerClient client = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString()); + BuiltinActionRegistry registry = new BuiltinActionRegistry() { + @Override + protected List buildDefinitions() { + return List.of( + definition("echo", buildMetaActionInfo("echo"), params -> params.get("value")) + ); + } + }; + client.setBuiltinActionRegistry(registry); + registry.getDefinitions().put( + "builtin::echo", + BuiltinActionRegistry.definition("echo", buildMetaActionInfo("echo"), params -> params.get("value")) + ); + + try { + MetaAction metaAction = buildMetaAction(MetaAction.Type.BUILTIN, "builtin", "echo", Map.of("value", "ok")); + RunnerClient.RunnerResponse response = client.doRun(metaAction); + Assertions.assertNotNull(response); + Assertions.assertTrue(response.isOk()); + Assertions.assertEquals("ok", response.getData()); + } finally { + executor.shutdownNow(); + } + } + + @Test + void testDoRunWithBuiltinMissingRegistry(@TempDir Path tempDir) { + ConcurrentHashMap existedMetaActions = new ConcurrentHashMap<>(); + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + LocalRunnerClient client = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString()); + + try { + MetaAction metaAction = buildMetaAction(MetaAction.Type.BUILTIN, "builtin", "echo", Map.of()); + RunnerClient.RunnerResponse response = client.doRun(metaAction); + Assertions.assertNotNull(response); + Assertions.assertFalse(response.isOk()); + Assertions.assertEquals("BuiltinActionRegistry 未初始化", response.getData()); + } finally { + executor.shutdownNow(); + } + } + @Test void testDoRunWithMcpLoadedFromCommonConfig(@TempDir Path tempDir) throws IOException, InterruptedException { ConcurrentHashMap existedMetaActions = new ConcurrentHashMap<>(); diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistryTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistryTest.java new file mode 100644 index 00000000..23e4e193 --- /dev/null +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/modules/action/builtin/BuiltinActionRegistryTest.java @@ -0,0 +1,126 @@ +package work.slhaf.partner.module.modules.action.builtin; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import work.slhaf.partner.core.action.ActionCapability; +import work.slhaf.partner.core.action.ActionCore; +import work.slhaf.partner.core.action.entity.MetaActionInfo; +import work.slhaf.partner.core.action.exception.MetaActionNotFoundException; +import work.slhaf.partner.core.action.runner.RunnerClient; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.*; + +class BuiltinActionRegistryTest { + + private static void injectActionCapability(BuiltinActionRegistry registry, ActionCapability actionCapability) throws Exception { + Field field = BuiltinActionRegistry.class.getDeclaredField("actionCapability"); + field.setAccessible(true); + field.set(registry, actionCapability); + } + + private static Map indexDefinitions( + List definitions + ) { + Map map = new HashMap<>(); + for (BuiltinActionRegistry.BuiltinActionDefinition definition : definitions) { + map.put(definition.actionKey(), definition); + } + return map; + } + + private static MetaActionInfo buildMetaActionInfo(String description) { + MetaActionInfo info = new MetaActionInfo(); + info.setDescription(description); + info.setParams(new HashMap<>()); + return info; + } + + @Test + void testInitRegistersMetaActionsAndMountsRunner() throws Exception { + ActionCapability actionCapability = mock(ActionCapability.class); + RunnerClient runnerClient = mock(RunnerClient.class); + when(actionCapability.runnerClient()).thenReturn(runnerClient); + + BuiltinActionRegistry registry = new TestRegistry(List.of( + BuiltinActionRegistry.definition("echo", buildMetaActionInfo("echo"), params -> params.get("value")) + )); + injectActionCapability(registry, actionCapability); + + registry.init(); + + verify(actionCapability).registerMetaActions(argThat(metaActions -> + metaActions.containsKey("builtin::echo") + && "echo".equals(metaActions.get("builtin::echo").getDescription()) + )); + verify(runnerClient).setBuiltinActionRegistry(registry); + } + + @Test + void testCallReturnsStringifiedResults() { + BuiltinActionRegistry registry = new TestRegistry(List.of( + BuiltinActionRegistry.definition("echo", buildMetaActionInfo("echo"), params -> params.get("value")), + BuiltinActionRegistry.definition("json", buildMetaActionInfo("json"), params -> Map.of("ok", true)), + BuiltinActionRegistry.definition("nil", buildMetaActionInfo("nil"), params -> null) + )); + + registry.getDefinitions().putAll(indexDefinitions(registry.buildDefinitions())); + + Assertions.assertEquals("hello", registry.call("builtin::echo", Map.of("value", "hello"))); + Assertions.assertEquals("{\"ok\":true}", registry.call("builtin::json", Map.of())); + Assertions.assertEquals("null", registry.call("builtin::nil", Map.of())); + } + + @Test + void testCallThrowsWhenMissingDefinition() { + BuiltinActionRegistry registry = new TestRegistry(List.of()); + Assertions.assertThrows(MetaActionNotFoundException.class, () -> registry.call("builtin::missing", Map.of())); + } + + @Test + void testCallPropagatesInvokerFailure() { + BuiltinActionRegistry registry = new TestRegistry(List.of( + BuiltinActionRegistry.definition("boom", buildMetaActionInfo("boom"), params -> { + throw new IllegalStateException("boom"); + }) + )); + registry.getDefinitions().putAll(indexDefinitions(registry.buildDefinitions())); + + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, + () -> registry.call("builtin::boom", Map.of())); + Assertions.assertEquals("boom", exception.getMessage()); + } + + @Test + void testActionCoreLoadsBuiltinMetaAction() throws Exception { + ActionCore actionCore = new ActionCore(); + try { + actionCore.registerMetaActions(Map.of("builtin::echo", buildMetaActionInfo("echo"))); + + Assertions.assertTrue(actionCore.listAvailableMetaActions().containsKey("builtin::echo")); + Assertions.assertEquals("echo", actionCore.loadMetaActionInfo("builtin::echo").getDescription()); + Assertions.assertEquals("builtin::echo", actionCore.loadMetaAction("builtin::echo").getKey()); + Assertions.assertEquals(ActionCore.BUILTIN_LOCATION, actionCore.loadMetaAction("builtin::echo").getLocation()); + } finally { + actionCore.getExecutor(ActionCore.ExecutorType.PLATFORM).shutdownNow(); + actionCore.getExecutor(ActionCore.ExecutorType.VIRTUAL).shutdownNow(); + } + } + + private static class TestRegistry extends BuiltinActionRegistry { + private final List definitions; + + private TestRegistry(List definitions) { + this.definitions = definitions; + } + + @Override + protected List buildDefinitions() { + return definitions; + } + } +}