From 70b8335d49050218c8891397794604ad90ed3fd0 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Sun, 11 Jan 2026 14:24:34 +0800 Subject: [PATCH] feat(LocalRunnerClient): support atomic persist serialization in LocalRunnerClient --- .../action/entity/ActionFileMetaData.java | 10 +++ .../ActionSerializeFailedException.java | 13 ++++ .../core/action/runner/LocalRunnerClient.java | 78 ++++++++++++++++++- .../action/runner/SandboxRunnerClient.java | 4 +- .../core/action/runner/RunnerClientTest.java | 4 +- 5 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 Partner-Main/src/main/java/work/slhaf/partner/core/action/entity/ActionFileMetaData.java create mode 100644 Partner-Main/src/main/java/work/slhaf/partner/core/action/exception/ActionSerializeFailedException.java diff --git a/Partner-Main/src/main/java/work/slhaf/partner/core/action/entity/ActionFileMetaData.java b/Partner-Main/src/main/java/work/slhaf/partner/core/action/entity/ActionFileMetaData.java new file mode 100644 index 00000000..18094e6b --- /dev/null +++ b/Partner-Main/src/main/java/work/slhaf/partner/core/action/entity/ActionFileMetaData.java @@ -0,0 +1,10 @@ +package work.slhaf.partner.core.action.entity; + +import lombok.Data; + +@Data +public class ActionFileMetaData { + private String content; + private String name; + private String ext; +} diff --git a/Partner-Main/src/main/java/work/slhaf/partner/core/action/exception/ActionSerializeFailedException.java b/Partner-Main/src/main/java/work/slhaf/partner/core/action/exception/ActionSerializeFailedException.java new file mode 100644 index 00000000..1f6d411f --- /dev/null +++ b/Partner-Main/src/main/java/work/slhaf/partner/core/action/exception/ActionSerializeFailedException.java @@ -0,0 +1,13 @@ +package work.slhaf.partner.core.action.exception; + +import work.slhaf.partner.api.agent.runtime.exception.AgentRuntimeException; + +public class ActionSerializeFailedException extends AgentRuntimeException { + public ActionSerializeFailedException(String message) { + super(message); + } + + public ActionSerializeFailedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java b/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java index 761f43ed..5578dab3 100644 --- a/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java +++ b/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/LocalRunnerClient.java @@ -27,11 +27,12 @@ import org.jetbrains.annotations.Nullable; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import work.slhaf.partner.common.mcp.InProcessMcpTransport; -import work.slhaf.partner.core.action.entity.McpData; +import work.slhaf.partner.core.action.entity.ActionFileMetaData; import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaActionInfo; import work.slhaf.partner.core.action.entity.MetaActionType; import work.slhaf.partner.core.action.exception.ActionInitFailedException; +import work.slhaf.partner.core.action.exception.ActionSerializeFailedException; import java.io.BufferedReader; import java.io.File; @@ -234,11 +235,80 @@ public class LocalRunnerClient extends RunnerClient { Files.writeString(path, code); } - @Override - public void persistSerialize(MetaActionInfo metaActionInfo, McpData mcpData) { - throw new UnsupportedOperationException("Unimplemented method 'doPersistSerialize'"); + private static @NotNull Path createActionDir(String baseName, Path baseDir) { + Path actionDir = null; + + // 原子地“抢占”目录名 + for (int i = 0; ; i++) { + String dirName = (i == 0) ? baseName : baseName + "(" + i + ")"; + Path candidate = baseDir.resolve(dirName); + + try { + Files.createDirectory(candidate); // 原子操作 + actionDir = candidate; + break; + } catch (FileAlreadyExistsException ignored) { + // 继续尝试下一个名字 + } catch (IOException e) { + throw new ActionSerializeFailedException( + "无法创建行动目录: " + candidate.toAbsolutePath(), e + ); + } + } + return actionDir; } + @Override + public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) { + val baseDir = Path.of(DYNAMIC_ACTION_PATH); + + if (!Files.isDirectory(baseDir)) { + throw new ActionSerializeFailedException( + "目录不存在或不可用: " + baseDir.toAbsolutePath() + ); + } + + val baseName = fileMetaData.getName(); + val ext = fileMetaData.getExt(); + + val actionDir = createActionDir(baseName, baseDir); + + // 使用临时文件写入内容 + val runTmp = actionDir.resolve("run." + ext + ".tmp"); + val descTmp = actionDir.resolve("desc.json.tmp"); + + val runFinal = actionDir.resolve("run." + ext); + val descFinal = actionDir.resolve("desc.json"); + + try { + Files.writeString(runTmp, fileMetaData.getContent()); + Files.writeString(descTmp, JSONObject.toJSONString(metaActionInfo)); + + // 原子提交 + Files.move(runTmp, runFinal, StandardCopyOption.ATOMIC_MOVE); + Files.move(descTmp, descFinal, StandardCopyOption.ATOMIC_MOVE); + + } catch (IOException e) { + // 失败清理 + safeDelete(runTmp); + safeDelete(descTmp); + safeDelete(runFinal); + safeDelete(descFinal); + safeDelete(actionDir); + throw new ActionSerializeFailedException("行动文件写入失败", e); + } + } + + private void safeDelete(Path path) { + try { + if (Files.exists(path)) { + Files.delete(path); + } + } catch (IOException ignored) { + } + } + + @Override public JSONObject listSysDependencies() { // 先只列出系统/环境的 Python 依赖 diff --git a/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java b/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java index 6d428bb6..410c1af9 100644 --- a/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java +++ b/Partner-Main/src/main/java/work/slhaf/partner/core/action/runner/SandboxRunnerClient.java @@ -1,7 +1,7 @@ package work.slhaf.partner.core.action.runner; import com.alibaba.fastjson2.JSONObject; -import work.slhaf.partner.core.action.entity.McpData; +import work.slhaf.partner.core.action.entity.ActionFileMetaData; import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaActionInfo; @@ -50,7 +50,7 @@ public class SandboxRunnerClient extends RunnerClient { } @Override - public void persistSerialize(MetaActionInfo metaActionInfo, McpData mcpData) { + public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) { throw new UnsupportedOperationException("Unimplemented method 'persistSerialize'"); } diff --git a/Partner-Main/src/test/java/work/slhaf/partner/core/action/runner/RunnerClientTest.java b/Partner-Main/src/test/java/work/slhaf/partner/core/action/runner/RunnerClientTest.java index 329e5aff..722c41b5 100644 --- a/Partner-Main/src/test/java/work/slhaf/partner/core/action/runner/RunnerClientTest.java +++ b/Partner-Main/src/test/java/work/slhaf/partner/core/action/runner/RunnerClientTest.java @@ -1,7 +1,7 @@ package work.slhaf.partner.core.action.runner; import com.alibaba.fastjson2.JSONObject; -import work.slhaf.partner.core.action.entity.McpData; +import work.slhaf.partner.core.action.entity.ActionFileMetaData; import work.slhaf.partner.core.action.entity.MetaAction; import work.slhaf.partner.core.action.entity.MetaActionInfo; @@ -33,7 +33,7 @@ public class RunnerClientTest { } @Override - public void persistSerialize(MetaActionInfo metaActionInfo, McpData mcpData) { + public void persistSerialize(MetaActionInfo metaActionInfo, ActionFileMetaData fileMetaData) { }