refactor(runner): adjust the directory organization of runner

This commit is contained in:
2026-03-12 15:32:28 +08:00
parent 0506149f5f
commit 9794b39572
17 changed files with 102 additions and 245 deletions

View File

@@ -9,6 +9,12 @@ import work.slhaf.partner.core.action.entity.ActionFileMetaData;
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.core.action.exception.ActionInitFailedException; import work.slhaf.partner.core.action.exception.ActionInitFailedException;
import work.slhaf.partner.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.execution.LocalProcessCommandExecutionService;
import work.slhaf.partner.core.action.runner.execution.McpActionExecutor;
import work.slhaf.partner.core.action.runner.execution.OriginExecutionService;
import work.slhaf.partner.core.action.runner.mcp.*;
import work.slhaf.partner.core.action.runner.support.ActionSerializer;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -22,8 +28,8 @@ import static work.slhaf.partner.common.util.PathUtil.buildPathStr;
@Slf4j @Slf4j
public class LocalRunnerClient extends RunnerClient implements AutoCloseable { public class LocalRunnerClient extends RunnerClient implements AutoCloseable {
static final String MCP_NAME_DESC = "mcp-desc"; public static final String MCP_NAME_DESC = "mcp-desc";
static final String MCP_NAME_DYNAMIC = "mcp-dynamic"; public static final String MCP_NAME_DYNAMIC = "mcp-dynamic";
private final String tmpActionPath; private final String tmpActionPath;
private final String dynamicActionPath; private final String dynamicActionPath;

View File

@@ -1,4 +0,0 @@
package work.slhaf.partner.core.action.runner;
interface RunnerExecutionPolicy {
}

View File

@@ -1,11 +1,11 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.execution;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
interface CommandExecutionService { public interface CommandExecutionService {
String[] buildCommands(String ext, Map<String, Object> params, String absolutePath); String[] buildCommands(String ext, Map<String, Object> params, String absolutePath);

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.execution;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@@ -7,7 +7,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
class LocalProcessCommandExecutionService implements CommandExecutionService { public class LocalProcessCommandExecutionService implements CommandExecutionService {
@Override @Override
public String[] buildCommands(String ext, Map<String, Object> params, String absolutePath) { public String[] buildCommands(String ext, Map<String, Object> params, String absolutePath) {

View File

@@ -1,21 +1,23 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.execution;
import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.runner.RunnerClient;
import work.slhaf.partner.core.action.runner.mcp.McpClientRegistry;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
class McpActionExecutor { public class McpActionExecutor {
private final McpClientRegistry mcpClientRegistry; private final McpClientRegistry mcpClientRegistry;
McpActionExecutor(McpClientRegistry mcpClientRegistry) { public McpActionExecutor(McpClientRegistry mcpClientRegistry) {
this.mcpClientRegistry = mcpClientRegistry; this.mcpClientRegistry = mcpClientRegistry;
} }
RunnerClient.RunnerResponse run(MetaAction metaAction) { public RunnerClient.RunnerResponse run(MetaAction metaAction) {
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse(); RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation()); McpSyncClient mcpClient = mcpClientRegistry.get(metaAction.getLocation());
if (mcpClient == null) { if (mcpClient == null) {

View File

@@ -1,19 +1,20 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.execution;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.runner.RunnerClient;
import java.io.File; import java.io.File;
class OriginExecutionService { public class OriginExecutionService {
private final CommandExecutionService commandExecutionService; private final CommandExecutionService commandExecutionService;
OriginExecutionService(CommandExecutionService commandExecutionService) { public OriginExecutionService(CommandExecutionService commandExecutionService) {
this.commandExecutionService = commandExecutionService; this.commandExecutionService = commandExecutionService;
} }
RunnerClient.RunnerResponse run(MetaAction metaAction) { public RunnerClient.RunnerResponse run(MetaAction metaAction) {
RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse(); RunnerClient.RunnerResponse response = new RunnerClient.RunnerResponse();
File file = new File(metaAction.getLocation()); File file = new File(metaAction.getLocation());
String ext = FileUtil.getSuffix(file); String ext = FileUtil.getSuffix(file);

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
@@ -15,6 +15,8 @@ import reactor.core.scheduler.Schedulers;
import work.slhaf.partner.common.mcp.InProcessMcpTransport; import work.slhaf.partner.common.mcp.InProcessMcpTransport;
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.core.action.runner.execution.CommandExecutionService;
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -33,7 +35,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@Slf4j @Slf4j
class DynamicActionMcpManager implements AutoCloseable { public class DynamicActionMcpManager implements AutoCloseable {
private final Path root; private final Path root;
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions; private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
@@ -42,7 +44,7 @@ class DynamicActionMcpManager implements AutoCloseable {
private final InProcessMcpTransport clientTransport; private final InProcessMcpTransport clientTransport;
private final DirectoryWatchSupport watchSupport; private final DirectoryWatchSupport watchSupport;
DynamicActionMcpManager(Path root, public DynamicActionMcpManager(Path root,
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
ExecutorService executor, ExecutorService executor,
CommandExecutionService commandExecutionService) throws IOException { CommandExecutionService commandExecutionService) throws IOException {
@@ -65,11 +67,11 @@ class DynamicActionMcpManager implements AutoCloseable {
.onOverflow((thisDir, context) -> reconcile()); .onOverflow((thisDir, context) -> reconcile());
} }
McpTransportConfig.InProcess clientConfig(int timeout) { public McpTransportConfig.InProcess clientConfig(int timeout) {
return new McpTransportConfig.InProcess(timeout, clientTransport); return new McpTransportConfig.InProcess(timeout, clientTransport);
} }
void start() { public void start() {
watchSupport.start(); watchSupport.start();
log.info("DynamicActionMcp 文件监听注册完毕"); log.info("DynamicActionMcp 文件监听注册完毕");
} }

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.McpSyncClient;
@@ -6,22 +6,22 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
class McpClientRegistry implements AutoCloseable { public class McpClientRegistry implements AutoCloseable {
private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, McpSyncClient> clients = new ConcurrentHashMap<>();
McpSyncClient get(String serverName) { public McpSyncClient get(String serverName) {
return clients.get(serverName); return clients.get(serverName);
} }
void register(String serverName, McpSyncClient client) { public void register(String serverName, McpSyncClient client) {
McpSyncClient old = clients.put(serverName, client); McpSyncClient old = clients.put(serverName, client);
if (old != null && old != client) { if (old != null && old != client) {
old.close(); old.close();
} }
} }
McpSyncClient remove(String serverName) { public McpSyncClient remove(String serverName) {
McpSyncClient client = detach(serverName); McpSyncClient client = detach(serverName);
if (client != null) { if (client != null) {
client.close(); client.close();
@@ -29,15 +29,15 @@ class McpClientRegistry implements AutoCloseable {
return client; return client;
} }
McpSyncClient detach(String serverName) { public McpSyncClient detach(String serverName) {
return clients.remove(serverName); return clients.remove(serverName);
} }
boolean contains(String serverName) { public boolean contains(String serverName) {
return clients.containsKey(serverName); return clients.containsKey(serverName);
} }
Set<String> listIds() { public Set<String> listIds() {
return new HashSet<>(clients.keySet()); return new HashSet<>(clients.keySet());
} }

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpClient;
@@ -6,6 +6,8 @@ import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -20,7 +22,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@Slf4j @Slf4j
class McpConfigWatcher implements AutoCloseable { public class McpConfigWatcher implements AutoCloseable {
private final Path root; private final Path root;
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions; private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
@@ -30,7 +32,7 @@ class McpConfigWatcher implements AutoCloseable {
private final DirectoryWatchSupport watchSupport; private final DirectoryWatchSupport watchSupport;
private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>(); private final Map<File, McpConfigFileRecord> mcpConfigFileCache = new HashMap<>();
McpConfigWatcher(Path root, public McpConfigWatcher(Path root,
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions, ConcurrentHashMap<String, MetaActionInfo> existedMetaActions,
McpClientRegistry mcpClientRegistry, McpClientRegistry mcpClientRegistry,
McpTransportFactory mcpTransportFactory, McpTransportFactory mcpTransportFactory,
@@ -48,7 +50,7 @@ class McpConfigWatcher implements AutoCloseable {
.onOverflow((thisDir, context) -> checkAndReload(false)); .onOverflow((thisDir, context) -> checkAndReload(false));
} }
void start() { public void start() {
watchSupport.start(); watchSupport.start();
log.info("CommonMcp 文件监听注册完毕"); log.info("CommonMcp 文件监听注册完毕");
} }

View File

@@ -1,6 +1,7 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -8,13 +9,13 @@ import java.nio.file.Path;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@Slf4j @Slf4j
class McpDescWatcher implements AutoCloseable { public class McpDescWatcher implements AutoCloseable {
private final Path root; private final Path root;
private final McpMetaRegistry mcpMetaRegistry; private final McpMetaRegistry mcpMetaRegistry;
private final DirectoryWatchSupport watchSupport; private final DirectoryWatchSupport watchSupport;
McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException { public McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
this.root = root; this.root = root;
this.mcpMetaRegistry = mcpMetaRegistry; this.mcpMetaRegistry = mcpMetaRegistry;
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, true, () -> mcpMetaRegistry.loadDirectory(root)) this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, true, () -> mcpMetaRegistry.loadDirectory(root))
@@ -24,7 +25,7 @@ class McpDescWatcher implements AutoCloseable {
.onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root)); .onOverflow((thisDir, context) -> mcpMetaRegistry.reconcile(root));
} }
void start() { public void start() {
watchSupport.start(); watchSupport.start();
log.info("DescMcp 文件监听注册完毕"); log.info("DescMcp 文件监听注册完毕");
} }

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
@@ -24,7 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@Slf4j @Slf4j
class McpMetaRegistry implements AutoCloseable { public class McpMetaRegistry implements AutoCloseable {
private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions; private final ConcurrentHashMap<String, MetaActionInfo> existedMetaActions;
private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, String> descCache = new ConcurrentHashMap<>();
@@ -33,7 +33,7 @@ class McpMetaRegistry implements AutoCloseable {
private final McpStatelessAsyncServer descServer; private final McpStatelessAsyncServer descServer;
private final InProcessMcpTransport clientTransport; private final InProcessMcpTransport clientTransport;
McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) { public McpMetaRegistry(ConcurrentHashMap<String, MetaActionInfo> existedMetaActions) {
this.existedMetaActions = existedMetaActions; this.existedMetaActions = existedMetaActions;
InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair(); InProcessMcpTransport.Pair pair = InProcessMcpTransport.pair();
this.clientTransport = pair.clientSide(); this.clientTransport = pair.clientSide();
@@ -46,11 +46,11 @@ class McpMetaRegistry implements AutoCloseable {
.build(); .build();
} }
McpTransportConfig.InProcess clientConfig(String serverName, int timeout) { public McpTransportConfig.InProcess clientConfig(String serverName, int timeout) {
return new McpTransportConfig.InProcess(timeout, clientTransport); return new McpTransportConfig.InProcess(timeout, clientTransport);
} }
void loadDirectory(Path root) { public void loadDirectory(Path root) {
File[] files = loadFiles(root); File[] files = loadFiles(root);
if (files == null) { if (files == null) {
return; return;
@@ -60,11 +60,11 @@ class McpMetaRegistry implements AutoCloseable {
} }
} }
boolean addOrUpdate(Path path) { public boolean addOrUpdate(Path path) {
return addOrUpdate(path.toFile()); return addOrUpdate(path.toFile());
} }
boolean addOrUpdate(File file) { public boolean addOrUpdate(File file) {
String name = file.getName(); String name = file.getName();
if (!isValidDescFile(name)) { if (!isValidDescFile(name)) {
return false; return false;
@@ -86,7 +86,7 @@ class McpMetaRegistry implements AutoCloseable {
} }
} }
void remove(Path path) { public void remove(Path path) {
String uri = path.toUri().toString(); String uri = path.toUri().toString();
String actionKey = path.getFileName().toString().replace(".desc.json", ""); String actionKey = path.getFileName().toString().replace(".desc.json", "");
descCache.remove(uri); descCache.remove(uri);
@@ -103,7 +103,7 @@ class McpMetaRegistry implements AutoCloseable {
} }
} }
void reconcile(Path root) { public void reconcile(Path root) {
File[] files = loadFiles(root); File[] files = loadFiles(root);
if (files == null) { if (files == null) {
return; return;
@@ -133,7 +133,7 @@ class McpMetaRegistry implements AutoCloseable {
} }
} }
MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) { public MetaActionInfo buildMetaActionInfo(String serverId, McpSchema.Tool tool) {
String actionKey = serverId + "::" + tool.name(); String actionKey = serverId + "::" + tool.name();
MetaActionInfo baseInfo = buildToolMetaActionInfo(tool); MetaActionInfo baseInfo = buildToolMetaActionInfo(tool);
originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo)); originalInfoCache.put(actionKey, copyMetaActionInfo(baseInfo));
@@ -161,7 +161,7 @@ class McpMetaRegistry implements AutoCloseable {
return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler); return new McpStatelessServerFeatures.AsyncResourceSpecification(resource, readHandler);
} }
boolean isValidDescFile(String fileName) { public boolean isValidDescFile(String fileName) {
return fileName.endsWith(".desc.json") && fileName.contains("::"); return fileName.endsWith(".desc.json") && fileName.contains("::");
} }

View File

@@ -1,11 +1,11 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import work.slhaf.partner.common.mcp.InProcessMcpTransport; import work.slhaf.partner.common.mcp.InProcessMcpTransport;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess { public sealed interface McpTransportConfig permits McpTransportConfig.Http, McpTransportConfig.Stdio, McpTransportConfig.InProcess {
int timeout(); int timeout();

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.mcp;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.ServerParameters;
@@ -7,13 +7,14 @@ import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequ
import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper; import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpClientTransport;
import work.slhaf.partner.core.action.runner.policy.RunnerExecutionPolicy;
import java.net.URI; import java.net.URI;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
class McpTransportFactory { public class McpTransportFactory {
McpClientTransport create(McpTransportConfig config, RunnerExecutionPolicy policy) { public McpClientTransport create(McpTransportConfig config, RunnerExecutionPolicy policy) {
return switch (config) { return switch (config) {
case McpTransportConfig.Stdio stdio -> { case McpTransportConfig.Stdio stdio -> {
ServerParameters serverParameters = ServerParameters.builder(stdio.command()) ServerParameters serverParameters = ServerParameters.builder(stdio.command())

View File

@@ -0,0 +1,4 @@
package work.slhaf.partner.core.action.runner.policy;
public interface RunnerExecutionPolicy {
}

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.support;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -17,17 +17,17 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
@Slf4j @Slf4j
class ActionSerializer { public class ActionSerializer {
private final String tmpActionPath; private final String tmpActionPath;
private final String dynamicActionPath; private final String dynamicActionPath;
ActionSerializer(String tmpActionPath, String dynamicActionPath) { public ActionSerializer(String tmpActionPath, String dynamicActionPath) {
this.tmpActionPath = tmpActionPath; this.tmpActionPath = tmpActionPath;
this.dynamicActionPath = dynamicActionPath; this.dynamicActionPath = dynamicActionPath;
} }
static String normalizeCodeType(String codeType) { public static String normalizeCodeType(String codeType) {
if (codeType == null || codeType.isBlank()) { if (codeType == null || codeType.isBlank()) {
throw new IllegalArgumentException("codeType 不能为空"); throw new IllegalArgumentException("codeType 不能为空");
} }
@@ -48,11 +48,11 @@ class ActionSerializer {
} }
} }
String buildTmpPath(String actionKey, String codeType) { public String buildTmpPath(String actionKey, String codeType) {
return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString(); return Path.of(tmpActionPath, System.currentTimeMillis() + "-" + actionKey + normalizeCodeType(codeType)).toString();
} }
void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException { public void tmpSerialize(MetaAction tempAction, String code, String codeType) throws IOException {
log.debug("行动程序临时序列化: {}", tempAction); log.debug("行动程序临时序列化: {}", tempAction);
Path path = Path.of(tempAction.getLocation()); Path path = Path.of(tempAction.getLocation());
validateTmpLocation(path, codeType); validateTmpLocation(path, codeType);
@@ -70,7 +70,7 @@ class ActionSerializer {
} }
} }
void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) { public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) {
log.debug("行动程序持久序列化: {}", metaActionInfo); log.debug("行动程序持久序列化: {}", metaActionInfo);
val baseDir = Path.of(dynamicActionPath); val baseDir = Path.of(dynamicActionPath);

View File

@@ -1,4 +1,4 @@
package work.slhaf.partner.core.action.runner; package work.slhaf.partner.core.action.runner.support;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -12,45 +12,46 @@ import java.util.stream.Stream;
import static java.nio.file.StandardWatchEventKinds.*; import static java.nio.file.StandardWatchEventKinds.*;
@Slf4j @Slf4j
class DirectoryWatchSupport implements Closeable { public class DirectoryWatchSupport implements Closeable {
private final Context ctx; private final Context ctx;
private final Map<WatchEvent.Kind<?>, EventHandler> handlers = new HashMap<>(); private final Map<WatchEvent.Kind<?>, EventHandler> handlers = new HashMap<>();
private final ExecutorService executor; private final ExecutorService executor;
private final boolean watchAll; private final boolean watchAll;
private final InitLoader initLoader; private final InitLoader initLoader;
DirectoryWatchSupport(Context ctx, ExecutorService executor, boolean watchAll, InitLoader initLoader) {
public DirectoryWatchSupport(Context ctx, ExecutorService executor, boolean watchAll, InitLoader initLoader) {
this.ctx = ctx; this.ctx = ctx;
this.executor = executor; this.executor = executor;
this.watchAll = watchAll; this.watchAll = watchAll;
this.initLoader = initLoader; this.initLoader = initLoader;
} }
DirectoryWatchSupport onCreate(EventHandler handler) { public DirectoryWatchSupport onCreate(EventHandler handler) {
ctx.kinds().add(ENTRY_CREATE); ctx.kinds().add(ENTRY_CREATE);
handlers.put(ENTRY_CREATE, handler); handlers.put(ENTRY_CREATE, handler);
return this; return this;
} }
DirectoryWatchSupport onModify(EventHandler handler) { public DirectoryWatchSupport onModify(EventHandler handler) {
ctx.kinds().add(ENTRY_MODIFY); ctx.kinds().add(ENTRY_MODIFY);
handlers.put(ENTRY_MODIFY, handler); handlers.put(ENTRY_MODIFY, handler);
return this; return this;
} }
DirectoryWatchSupport onDelete(EventHandler handler) { public DirectoryWatchSupport onDelete(EventHandler handler) {
ctx.kinds().add(ENTRY_DELETE); ctx.kinds().add(ENTRY_DELETE);
handlers.put(ENTRY_DELETE, handler); handlers.put(ENTRY_DELETE, handler);
return this; return this;
} }
DirectoryWatchSupport onOverflow(EventHandler handler) { public DirectoryWatchSupport onOverflow(EventHandler handler) {
ctx.kinds().add(OVERFLOW); ctx.kinds().add(OVERFLOW);
handlers.put(OVERFLOW, handler); handlers.put(OVERFLOW, handler);
return this; return this;
} }
void start() { public void start() {
registerPath(); registerPath();
if (initLoader != null) { if (initLoader != null) {
initLoader.load(); initLoader.load();
@@ -58,15 +59,15 @@ class DirectoryWatchSupport implements Closeable {
executor.execute(buildWatchTask()); executor.execute(buildWatchTask());
} }
Context context() { public Context context() {
return ctx; return ctx;
} }
boolean isWatching(Path dir) { public boolean isWatching(Path dir) {
return ctx.watchKeys().values().stream().anyMatch(dir::equals); return ctx.watchKeys().values().stream().anyMatch(dir::equals);
} }
void registerDirectory(Path dir) throws IOException { public void registerDirectory(Path dir) throws IOException {
if (!java.nio.file.Files.isDirectory(dir) || isWatching(dir)) { if (!java.nio.file.Files.isDirectory(dir) || isWatching(dir)) {
return; return;
} }
@@ -148,16 +149,17 @@ class DirectoryWatchSupport implements Closeable {
ctx.watchKeys().clear(); ctx.watchKeys().clear();
} }
interface EventHandler { public interface EventHandler {
void handle(Path thisDir, Path context); void handle(Path thisDir, Path context);
} }
interface InitLoader { public interface InitLoader {
void load(); void load();
} }
record Context(Path root, WatchService watchService, Map<WatchKey, Path> watchKeys, Set<WatchEvent.Kind<?>> kinds) { public record Context(Path root, WatchService watchService, Map<WatchKey, Path> watchKeys,
Context(Path root) throws IOException { Set<WatchEvent.Kind<?>> kinds) {
public Context(Path root) throws IOException {
this(root, FileSystems.getDefault().newWatchService(), new HashMap<>(), new LinkedHashSet<>()); this(root, FileSystems.getDefault().newWatchService(), new HashMap<>(), new LinkedHashSet<>());
} }
} }

View File

@@ -1,160 +0,0 @@
package work.slhaf.partner.core.action.runner;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.spec.McpSchema;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
import work.slhaf.partner.core.action.entity.MetaAction;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class RunnerStabilizationTest {
@Test
void actionSerializerUsesNormalizedCodeType(@TempDir Path tempDir) throws Exception {
ActionSerializer serializer = new ActionSerializer(tempDir.toString(), tempDir.toString());
String builtPath = serializer.buildTmpPath("demo", "py");
Assertions.assertTrue(builtPath.endsWith(".py"));
MetaAction metaAction = new MetaAction("demo", false, MetaAction.Type.ORIGIN, builtPath);
serializer.tmpSerialize(metaAction, "print('ok')", ".py");
Assertions.assertTrue(Files.exists(Path.of(builtPath)));
Assertions.assertEquals("print('ok')", Files.readString(Path.of(builtPath)));
Assertions.assertThrows(Exception.class, () -> serializer.tmpSerialize(metaAction, "print('bad')", ".sh"));
}
@Test
void mcpTransportConfigHasValueEquality() {
McpTransportConfig.Stdio left = new McpTransportConfig.Stdio(30, "npx", Map.of("A", "1"), List.of("-y", "demo"));
McpTransportConfig.Stdio right = new McpTransportConfig.Stdio(30, "npx", Map.of("A", "1"), List.of("-y", "demo"));
Assertions.assertEquals(left, right);
Assertions.assertEquals(left.hashCode(), right.hashCode());
}
@Test
void mcpConfigWatcherReadParamsAcceptsTimeout(@TempDir Path tempDir) throws Exception {
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
McpConfigWatcher watcher = new McpConfigWatcher(
tempDir,
existedMetaActions,
new McpClientRegistry(),
new McpTransportFactory(),
new McpMetaRegistry(existedMetaActions),
executor
);
try {
Method readParams = McpConfigWatcher.class.getDeclaredMethod("readParams", cn.hutool.json.JSONObject.class);
readParams.setAccessible(true);
cn.hutool.json.JSONObject stdioJson = cn.hutool.json.JSONUtil.parseObj("""
{
"command": "npx",
"args": ["-y", "demo"],
"env": {},
"timeout": 45
}
""");
Object stdioConfig = readParams.invoke(watcher, stdioJson);
Assertions.assertInstanceOf(McpTransportConfig.Stdio.class, stdioConfig);
Assertions.assertEquals(45, ((McpTransportConfig.Stdio) stdioConfig).timeout());
} finally {
watcher.close();
executor.shutdownNow();
}
}
@Test
void localRunnerClientCloseIsIdempotent(@TempDir Path tempDir) {
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
LocalRunnerClient client = new LocalRunnerClient(existedMetaActions, executor, tempDir.toString());
try {
client.close();
client.close();
} finally {
executor.shutdownNow();
}
}
@Test
void mcpActionExecutorUsesStructuredContentThenTextContent() {
McpClientRegistry registry = new McpClientRegistry();
McpSyncClient client = Mockito.mock(McpSyncClient.class);
registry.register("demo", client);
McpActionExecutor executor = new McpActionExecutor(registry);
MetaAction metaAction = new MetaAction("tool", false, MetaAction.Type.MCP, "demo");
Mockito.when(client.callTool(Mockito.any())).thenReturn(
new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("hello")), null, null, Map.of())
);
RunnerClient.RunnerResponse textResponse = executor.run(metaAction);
Assertions.assertTrue(textResponse.isOk());
Assertions.assertEquals("hello", textResponse.getData());
Mockito.when(client.callTool(Mockito.any())).thenReturn(
new McpSchema.CallToolResult(List.of(), Boolean.FALSE, Map.of("k", "v"), Map.of())
);
RunnerClient.RunnerResponse structuredResponse = executor.run(metaAction);
Assertions.assertTrue(structuredResponse.isOk());
Assertions.assertEquals("{k=v}", structuredResponse.getData());
}
@Test
void mcpMetaRegistryFallsBackToOriginalToolMetaAfterDescRemoval(@TempDir Path tempDir) throws Exception {
ConcurrentHashMap<String, MetaActionInfo> existedMetaActions = new ConcurrentHashMap<>();
McpMetaRegistry registry = new McpMetaRegistry(existedMetaActions);
try {
McpSchema.Tool tool = McpSchema.Tool.builder()
.name("tool")
.description("tool description")
.inputSchema(McpJsonMapper.getDefault(), "{\"type\":\"object\",\"properties\":{}}")
.outputSchema(Map.of("type", "string"))
.meta(Map.of("io", true, "pre", List.of("pre"), "post", List.of("post"), "strict", true, "tag", List.of("tag")))
.build();
MetaActionInfo baseInfo = registry.buildMetaActionInfo("demo", tool);
existedMetaActions.put("demo::tool", baseInfo);
Path descFile = tempDir.resolve("demo::tool.desc.json");
Files.writeString(descFile, """
{
"io": false,
"params": {},
"description": "desc override",
"tags": ["desc"],
"preActions": [],
"postActions": [],
"strictDependencies": false,
"responseSchema": {}
}
""");
Assertions.assertTrue(registry.addOrUpdate(descFile));
Assertions.assertEquals("desc override", existedMetaActions.get("demo::tool").getDescription());
registry.remove(descFile);
MetaActionInfo restoredInfo = existedMetaActions.get("demo::tool");
Assertions.assertEquals("tool description", restoredInfo.getDescription());
Assertions.assertTrue(restoredInfo.isIo());
Assertions.assertEquals(List.of("tag"), restoredInfo.getTags());
} finally {
registry.close();
}
}
}