package com.agent.management.engine; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.regex.Pattern; /** * 工作流运行目录管理器 * 为每次工作流执行创建独立的工作目录,永久保留 * *

目录结构: *

*/ @Slf4j @Component public class WorkflowRunDirManager { /** runId 白名单:UUID 截取的 8~16 位十六进制 */ private static final Pattern RUN_ID_PATTERN = Pattern.compile("^[a-fA-F0-9]{1,16}$"); private final Path basePath; public WorkflowRunDirManager( @Value("${workflow.run-dir:./data/workflow-runs}") String basePath) { this.basePath = Paths.get(basePath).toAbsolutePath().normalize(); try { Files.createDirectories(this.basePath); log.info("[RunDirManager] 工作目录基路径: {}", this.basePath); } catch (IOException e) { throw new IllegalStateException("无法创建工作流运行目录: " + this.basePath, e); } } /** * 为指定工作流执行创建工作目录(新建使用 workflowName 作为目录段)。 * * @param workflowId 工作流内部 ID(兼容旧目录兜底用) * @param workflowName 工作流唯一标识(kebab-case),新建目录使用此字段 * @param runId 运行 ID(必须为 1~16 位十六进制) */ public Path createRunDir(Long workflowId, String workflowName, String runId) { validateRunId(runId); Path runDir = resolveByName(workflowName, runId); try { Files.createDirectories(runDir); log.info("[RunDirManager] 创建运行目录: {}", runDir); return runDir; } catch (IOException e) { throw new RuntimeException("创建运行目录失败: " + runDir, e); } } /** * 获取指定运行的工作目录(已存在时返回,不存在返回 null)。 * 查询顺序:先按 name,再按 id 兜底。 */ public Path getRunDir(Long workflowId, String workflowName, String runId) { try { validateRunId(runId); } catch (IllegalArgumentException e) { return null; } // 1. 优先按 name 查找 if (workflowName != null && !workflowName.isEmpty()) { Path byName = resolveByName(workflowName, runId); if (Files.isDirectory(byName)) return byName; } // 2. 回退按 id 查找(兼容旧目录) if (workflowId != null) { Path byId = resolveById(workflowId, runId); if (Files.isDirectory(byId)) return byId; } return null; } /** * 兼容旧调用:仅按 id 创建/查询。新代码应使用带 name 的重载。 */ @Deprecated public Path createRunDir(Long workflowId, String runId) { return createRunDir(workflowId, String.valueOf(workflowId), runId); } @Deprecated public Path getRunDir(Long workflowId, String runId) { return getRunDir(workflowId, null, runId); } /** * 获取基路径 */ public Path getBasePath() { return basePath; } /** * 校验 runId 格式,防止路径穿越 */ private void validateRunId(String runId) { if (runId == null || !RUN_ID_PATTERN.matcher(runId).matches()) { throw new IllegalArgumentException("非法 runId: " + runId); } } private Path resolveByName(String workflowName, String runId) { Path runDir = basePath.resolve(workflowName).resolve(runId).normalize(); if (!runDir.startsWith(basePath)) { throw new IllegalArgumentException("解析后的 runId 路径越界: " + runDir); } return runDir; } private Path resolveById(Long workflowId, String runId) { Path runDir = basePath.resolve(String.valueOf(workflowId)).resolve(runId).normalize(); if (!runDir.startsWith(basePath)) { throw new IllegalArgumentException("解析后的 runId 路径越界: " + runDir); } return runDir; } }