refactor(action): support built-in actions

This commit is contained in:
2026-03-12 10:41:17 +08:00
parent 3c550af33d
commit 6c8a1b2636
10 changed files with 310 additions and 19 deletions

19
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EntryPointsManager"> <component name="EntryPointsManager">
<list size="15"> <list size="16">
<item index="0" class="java.lang.String" itemvalue="lombok.Data" /> <item index="0" class="java.lang.String" itemvalue="lombok.Data" />
<item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" /> <item index="1" class="java.lang.String" itemvalue="net.bytebuddy.implementation.bind.annotation.RuntimeType" />
<item index="2" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" /> <item index="2" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Capability" />
@@ -9,14 +9,15 @@
<item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" /> <item index="4" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CapabilityMethod" />
<item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" /> <item index="5" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.CoordinateManager" />
<item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" /> <item index="6" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.capability.annotation.Coordinated" />
<item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" /> <item index="7" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.component.annotation.Init" />
<item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" /> <item index="8" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AfterExecute" />
<item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" /> <item index="9" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentRunningModule" />
<item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" /> <item index="10" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.AgentSubModule" />
<item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" /> <item index="11" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.BeforeExecute" />
<item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" /> <item index="12" class="java.lang.String" itemvalue="work.slhaf.partner.api.agent.factory.module.annotation.Init" />
<item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" /> <item index="13" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CapabilityMethod" />
<item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" /> <item index="14" class="java.lang.String" itemvalue="work.slhaf.partner.api.capability.annotation.CoordinateManager" />
<item index="15" class="java.lang.String" itemvalue="work.slhaf.partner.api.register.capability.annotation.Capability" />
</list> </list>
<writeAnnotations> <writeAnnotations>
<writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" /> <writeAnnotation name="work.slhaf.partner.api.agent.factory.capability.annotation.InjectCapability" />

View File

@@ -53,6 +53,8 @@ public interface ActionCapability {
MetaActionInfo loadMetaActionInfo(@NonNull String actionKey); MetaActionInfo loadMetaActionInfo(@NonNull String actionKey);
void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions);
Map<String, MetaActionInfo> listAvailableMetaActions(); Map<String, MetaActionInfo> listAvailableMetaActions();
boolean checkExists(String... actionKeys); boolean checkExists(String... actionKeys);

View File

@@ -30,6 +30,7 @@ import java.util.stream.Collectors;
@CapabilityCore(value = "action") @CapabilityCore(value = "action")
@Slf4j @Slf4j
public class ActionCore extends PartnerCore<ActionCore> { public class ActionCore extends PartnerCore<ActionCore> {
public static final String BUILTIN_LOCATION = "builtin";
private final Lock cacheLock = new ReentrantLock(); private final Lock cacheLock = new ReentrantLock();
// 由于当前的执行器逻辑实现,平台线程池大小不得小于 2这里规定为最小为 4 // 由于当前的执行器逻辑实现,平台线程池大小不得小于 2这里规定为最小为 4
@@ -273,7 +274,12 @@ public class ActionCore extends PartnerCore<ActionCore> {
} }
@CapabilityMethod @CapabilityMethod
public Map<String, MetaActionInfo> listAvailableActions() { public void registerMetaActions(@NonNull Map<String, MetaActionInfo> metaActions) {
existedMetaActions.putAll(metaActions);
}
@CapabilityMethod
public Map<String, MetaActionInfo> listAvailableMetaActions() {
return existedMetaActions; return existedMetaActions;
} }
@@ -320,10 +326,11 @@ public class ActionCore extends PartnerCore<ActionCore> {
if (split.length < 2) { if (split.length < 2) {
throw new MetaActionNotFoundException("未找到对应的行动程序,原因: 传入的 actionKey(" + actionKey + ") 存在异常"); throw new MetaActionNotFoundException("未找到对应的行动程序,原因: 传入的 actionKey(" + actionKey + ") 存在异常");
} }
MetaAction.Type type = BUILTIN_LOCATION.equals(split[0]) ? MetaAction.Type.BUILTIN : MetaAction.Type.MCP;
return new MetaAction( return new MetaAction(
split[1], split[1],
metaActionInfo.isIo(), metaActionInfo.isIo(),
MetaAction.Type.MCP, type,
split[0] split[0]
); );
} }

View File

@@ -14,12 +14,14 @@ data class MetaAction(
*/ */
val io: Boolean = false, val io: Boolean = false,
/** /**
* 行动程序类型,可分为 MCP、ORIGIN 两种,前者对应读取到的 MCP Tool、后者对应生成的临时行动程序 * 行动程序类型,可分为 MCP、ORIGIN、BUILTIN 三种,
* 分别对应读取到的 MCP Tool、生成的临时行动程序、本地内置行动
*/ */
val type: Type, val type: Type,
/** /**
* 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id; * 当类型为 MCP 时,该字段对应相应 MCP Client 注册时生成的 id;
* 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串 * 当类型为 ORIGIN 时,该字段对应相应的磁盘路径字符串;
* 当类型为 BUILTIN 时,该字段固定为 builtin
*/ */
val location: String, val location: String,
) { ) {
@@ -67,7 +69,12 @@ data class MetaAction(
/** /**
* 适用于‘临时生成’的行动程序,在生成后根据序列化选项及执行情况,进行持久化 * 适用于‘临时生成’的行动程序,在生成后根据序列化选项及执行情况,进行持久化
*/ */
ORIGIN ORIGIN,
/**
* 由本地内置注册表直接执行的行动
*/
BUILTIN
} }
} }

View File

@@ -224,6 +224,7 @@ public class LocalRunnerClient extends RunnerClient {
response = switch (metaAction.getType()) { response = switch (metaAction.getType()) {
case MetaAction.Type.MCP -> doRunWithMcp(metaAction); case MetaAction.Type.MCP -> doRunWithMcp(metaAction);
case MetaAction.Type.ORIGIN -> doRunWithOrigin(metaAction); case MetaAction.Type.ORIGIN -> doRunWithOrigin(metaAction);
case MetaAction.Type.BUILTIN -> doRunWithBuiltin(metaAction);
}; };
} catch (Exception e) { } catch (Exception e) {
response = new RunnerResponse(); response = new RunnerResponse();

View File

@@ -1,8 +1,8 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import io.modelcontextprotocol.server.McpStatelessAsyncServer;
import lombok.Data; import lombok.Data;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import lombok.val; import lombok.val;
import org.jetbrains.annotations.Nullable; 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.MetaAction.Result;
import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.exception.ActionInitFailedException; import work.slhaf.partner.core.action.exception.ActionInitFailedException;
import work.slhaf.partner.module.modules.action.builtin.BuiltinActionRegistry;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -45,8 +46,8 @@ public abstract class RunnerClient {
protected final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions; protected final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
protected final ExecutorService executor; protected final ExecutorService executor;
//TODO 仍可提供内部 MCP但调用方式需要结合 AgentContext来获取否则生命周期不合 @Setter
protected McpStatelessAsyncServer innerMcpServer; protected BuiltinActionRegistry builtinActionRegistry;
/** /**
* ActionCore 将注入虚拟线程池 * ActionCore 将注入虚拟线程池
@@ -82,6 +83,23 @@ public abstract class RunnerClient {
public abstract void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData); 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) { protected void createPath(String pathStr) {
val path = Path.of(pathStr); val path = Path.of(pathStr);
try { try {

View File

@@ -30,8 +30,13 @@ public class SandboxRunnerClient extends RunnerClient {
} }
protected RunnerResponse doRun(MetaAction metaAction) { protected RunnerResponse doRun(MetaAction metaAction) {
return switch (metaAction.getType()) {
case BUILTIN -> doRunWithBuiltin(metaAction);
case MCP, ORIGIN -> {
// 调用沙盒执行器 // 调用沙盒执行器
return null; yield null;
}
};
} }
@Override @Override

View File

@@ -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<String, BuiltinActionDefinition> definitions = new LinkedHashMap<>();
@InjectCapability
private ActionCapability actionCapability;
public static BuiltinActionDefinition definition(String name, MetaActionInfo metaActionInfo,
Function<Map<String, Object>, 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<BuiltinActionDefinition> buildDefinitions() {
return List.of();
}
public String call(@NonNull String actionKey, @NonNull Map<String, Object> 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<String, MetaActionInfo> exportMetaActionInfos() {
Map<String, MetaActionInfo> metaActions = new LinkedHashMap<>();
definitions.forEach((key, value) -> metaActions.put(key, value.metaActionInfo()));
return metaActions;
}
public record BuiltinActionDefinition(
String actionKey,
MetaActionInfo metaActionInfo,
Function<Map<String, Object>, Object> invoker
) {
}
}

View File

@@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.module.modules.action.builtin.BuiltinActionRegistry;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -843,6 +844,53 @@ public class LocalRunnerClientTest {
} }
} }
@Test
void testDoRunWithBuiltin(@TempDir Path tempDir) {
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
LocalRunnerClient client = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
BuiltinActionRegistry registry = new BuiltinActionRegistry() {
@Override
protected List<BuiltinActionDefinition> 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<String, MetaActionInfo> 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 @Test
void testDoRunWithMcpLoadedFromCommonConfig(@TempDir Path tempDir) throws IOException, InterruptedException { void testDoRunWithMcpLoadedFromCommonConfig(@TempDir Path tempDir) throws IOException, InterruptedException {
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>(); ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();

View File

@@ -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<String, BuiltinActionRegistry.BuiltinActionDefinition> indexDefinitions(
List<BuiltinActionRegistry.BuiltinActionDefinition> definitions
) {
Map<String, BuiltinActionRegistry.BuiltinActionDefinition> 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<BuiltinActionDefinition> definitions;
private TestRegistry(List<BuiltinActionDefinition> definitions) {
this.definitions = definitions;
}
@Override
protected List<BuiltinActionDefinition> buildDefinitions() {
return definitions;
}
}
}