refactor(watch): support configurable directory watch depth

This commit is contained in:
2026-04-01 22:15:00 +08:00
parent 632e47ec13
commit 4ae65b885e
7 changed files with 322 additions and 20 deletions

View File

@@ -11,11 +11,11 @@ import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import work.slhaf.partner.api.common.support.DirectoryWatchSupport;
import work.slhaf.partner.common.mcp.InProcessMcpTransport;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
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.IOException;
@@ -58,7 +58,7 @@ public class DynamicActionMcpManager implements AutoCloseable {
.capabilities(serverCapabilities)
.jsonMapper(McpJsonMapper.getDefault())
.build();
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, true, this::loadExisting)
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 1, this::loadExisting)
.onCreate(this::handleCreate)
.onModify(this::handleModify)
.onDelete(this::handleDelete)

View File

@@ -6,11 +6,11 @@ import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import work.slhaf.partner.api.common.support.DirectoryWatchSupport;
import work.slhaf.partner.core.action.entity.MetaActionInfo;
import work.slhaf.partner.core.action.runner.LocalRunnerClient;
import work.slhaf.partner.core.action.runner.policy.ExecutionPolicy;
import work.slhaf.partner.core.action.runner.policy.RunnerExecutionPolicyListener;
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
import java.io.File;
import java.io.IOException;
@@ -46,7 +46,7 @@ public class McpConfigWatcher implements AutoCloseable, RunnerExecutionPolicyLis
this.mcpClientRegistry = mcpClientRegistry;
this.mcpTransportFactory = mcpTransportFactory;
this.mcpMetaRegistry = mcpMetaRegistry;
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, false, this::loadInitial)
this.watchSupport = new DirectoryWatchSupport(new DirectoryWatchSupport.Context(root), executor, 0, this::loadInitial)
.onCreate(this::handleCreate)
.onModify((thisDir, context) -> checkAndReload(true))
.onDelete(this::handleDelete)

View File

@@ -1,7 +1,7 @@
package work.slhaf.partner.core.action.runner.mcp;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.partner.core.action.runner.support.DirectoryWatchSupport;
import work.slhaf.partner.api.common.support.DirectoryWatchSupport;
import java.io.IOException;
import java.nio.file.Files;
@@ -18,7 +18,7 @@ public class McpDescWatcher implements AutoCloseable {
public McpDescWatcher(Path root, McpMetaRegistry mcpMetaRegistry, ExecutorService executor) throws IOException {
this.root = root;
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, 0, () -> mcpMetaRegistry.loadDirectory(root))
.onCreate(this::handleUpsert)
.onModify(this::handleUpsert)
.onDelete(this::handleDelete)

View File

@@ -1,166 +0,0 @@
package work.slhaf.partner.core.action.runner.support;
import lombok.extern.slf4j.Slf4j;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.stream.Stream;
import static java.nio.file.StandardWatchEventKinds.*;
@Slf4j
public class DirectoryWatchSupport implements Closeable {
private final Context ctx;
private final Map<WatchEvent.Kind<?>, EventHandler> handlers = new HashMap<>();
private final ExecutorService executor;
private final boolean watchAll;
private final InitLoader initLoader;
public DirectoryWatchSupport(Context ctx, ExecutorService executor, boolean watchAll, InitLoader initLoader) {
this.ctx = ctx;
this.executor = executor;
this.watchAll = watchAll;
this.initLoader = initLoader;
}
public DirectoryWatchSupport onCreate(EventHandler handler) {
ctx.kinds().add(ENTRY_CREATE);
handlers.put(ENTRY_CREATE, handler);
return this;
}
public DirectoryWatchSupport onModify(EventHandler handler) {
ctx.kinds().add(ENTRY_MODIFY);
handlers.put(ENTRY_MODIFY, handler);
return this;
}
public DirectoryWatchSupport onDelete(EventHandler handler) {
ctx.kinds().add(ENTRY_DELETE);
handlers.put(ENTRY_DELETE, handler);
return this;
}
public DirectoryWatchSupport onOverflow(EventHandler handler) {
ctx.kinds().add(OVERFLOW);
handlers.put(OVERFLOW, handler);
return this;
}
public void start() {
registerPath();
if (initLoader != null) {
initLoader.load();
}
executor.execute(buildWatchTask());
}
public Context context() {
return ctx;
}
public boolean isWatching(Path dir) {
return ctx.watchKeys().values().stream().anyMatch(dir::equals);
}
public void registerDirectory(Path dir) throws IOException {
if (!java.nio.file.Files.isDirectory(dir) || isWatching(dir)) {
return;
}
WatchEvent.Kind<?>[] kindsArray = ctx.kinds().toArray(WatchEvent.Kind[]::new);
WatchKey key = dir.register(ctx.watchService(), kindsArray);
ctx.watchKeys().put(key, dir);
}
private void registerPath() {
try {
registerDirectory(ctx.root());
if (!watchAll) {
return;
}
try (Stream<Path> walk = Files.list(ctx.root()).filter(Files::isDirectory)) {
for (Path dir : walk.toList()) {
registerDirectory(dir);
}
}
} catch (IOException e) {
log.error("监听目录注册失败: ", e);
}
}
private Runnable buildWatchTask() {
return () -> {
String rootStr = ctx.root().toString();
log.info("行动程序目录监听器已启动,监听目录: {}", rootStr);
while (true) {
WatchKey key = null;
try {
key = ctx.watchService().take();
List<WatchEvent<?>> events = key.pollEvents();
for (WatchEvent<?> event : events) {
WatchEvent.Kind<?> kind = event.kind();
Object context = event.context();
log.debug("文件目录监听事件: {} - {} - {}", rootStr, kind.name(), context);
Path thisDir = (Path) key.watchable();
EventHandler handler = handlers.get(kind);
if (handler == null) {
continue;
}
handler.handle(thisDir, context instanceof Path path ? thisDir.resolve(path) : null);
}
} catch (InterruptedException e) {
log.info("监听线程被中断,准备退出...");
Thread.currentThread().interrupt();
break;
} catch (ClosedWatchServiceException e) {
log.info("WatchService 已关闭,监听线程退出。");
break;
} finally {
if (key != null) {
boolean valid = key.reset();
if (!valid) {
log.info("WatchKey 已失效,停止监听该目录: {}", key.watchable());
ctx.watchKeys().remove(key);
if (key.watchable().equals(ctx.root())) {
try {
Files.createDirectories(ctx.root());
registerPath();
if (initLoader != null) {
initLoader.load();
}
} catch (IOException e) {
log.error("重建根目录并重新注册监听失败: {}", ctx.root(), e);
}
}
}
}
}
}
};
}
@Override
public void close() throws IOException {
ctx.watchService().close();
ctx.watchKeys().clear();
}
public interface EventHandler {
void handle(Path thisDir, Path context);
}
public interface InitLoader {
void load();
}
public record Context(Path root, WatchService watchService, Map<WatchKey, Path> watchKeys,
Set<WatchEvent.Kind<?>> kinds) {
public Context(Path root) throws IOException {
this(root, FileSystems.getDefault().newWatchService(), new HashMap<>(), new LinkedHashSet<>());
}
}
}