|
|
@@ -56,16 +56,17 @@ import java.util.zip.ZipOutputStream;
|
|
|
/**
|
|
|
* 外部 API(/api/v1/**)入口:第三方系统通过此 Controller 调用平台工作流。
|
|
|
*
|
|
|
+ * <p>URL 路径使用 kebab-case 唯一标识 {@code workflowName}(与 Skill 一致)。
|
|
|
+ * 旧调用方使用数字 id 的 URL 仍可通过 by-id 兜底逻辑访问(用户手动改名为非数字前都自动兼容)。
|
|
|
+ *
|
|
|
* 流程:
|
|
|
- * - POST /workflows/{id}/runs 创建运行(提交输入与文件,返回 runId)
|
|
|
- * - POST /workflows/{id}/runs/{runId}/start 启动执行
|
|
|
- * - GET /workflows/{id}/runs/{runId}/stream 订阅 SSE 流(思考过程 + 节点状态)
|
|
|
- * - GET /workflows/{id}/runs/{runId} 查询运行结果
|
|
|
- * - GET /workflows/{id}/runs/{runId}/workspace 下载工作空间 zip
|
|
|
- * - GET /workflows/{id}/runs/{runId}/nodes/{nodeId}/logs 查询节点日志
|
|
|
+ * - POST /workflows/{workflowName}/runs 创建运行
|
|
|
+ * - POST /workflows/{workflowName}/runs/{runId}/start 启动执行
|
|
|
+ * - GET /workflows/{workflowName}/runs/{runId}/stream 订阅 SSE 流
|
|
|
+ * - GET /workflows/{workflowName}/runs/{runId} 查询运行结果
|
|
|
+ * - GET /workflows/{workflowName}/runs/{runId}/workspace 下载工作空间 zip
|
|
|
+ * - GET /workflows/{workflowName}/runs/{runId}/nodes/{nodeId}/logs 查询节点日志
|
|
|
* - GET /workflows 列出当前 API Key 可调用的工作流
|
|
|
- *
|
|
|
- * 鉴权:见 {@link ExternalApiAuthFilter},匹配的 KeyEntry 放入请求属性。
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@RestController
|
|
|
@@ -88,38 +89,32 @@ public class ExternalApiController {
|
|
|
private static final long SSE_TIMEOUT_MS = 1_800_000L;
|
|
|
|
|
|
// ============================================================
|
|
|
- // POST /workflows/{id}/runs — 创建运行
|
|
|
+ // POST /workflows/{workflowName}/runs — 创建运行
|
|
|
// ============================================================
|
|
|
|
|
|
- /**
|
|
|
- * 形态 A:仅 JSON 输入
|
|
|
- */
|
|
|
- @PostMapping(value = "/workflows/{workflowId}/runs", consumes = MediaType.APPLICATION_JSON_VALUE)
|
|
|
- public ResponseEntity<?> createRunJson(@PathVariable Long workflowId,
|
|
|
+ @PostMapping(value = "/workflows/{workflowName}/runs", consumes = MediaType.APPLICATION_JSON_VALUE)
|
|
|
+ public ResponseEntity<?> createRunJson(@PathVariable String workflowName,
|
|
|
@RequestAttribute(ExternalApiAuthFilter.ATTR_KEY_ENTRY) ExternalApiProperties.KeyEntry keyEntry,
|
|
|
@RequestBody(required = false) CreateRunRequest req) {
|
|
|
- return doCreateRun(workflowId, req, null, null);
|
|
|
+ return doCreateRun(workflowName, req, null, null);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 形态 B:multipart(JSON payload + 文件/目录)
|
|
|
- */
|
|
|
- @PostMapping(value = "/workflows/{workflowId}/runs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
|
- public ResponseEntity<?> createRunMultipart(@PathVariable Long workflowId,
|
|
|
+ @PostMapping(value = "/workflows/{workflowName}/runs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
|
+ public ResponseEntity<?> createRunMultipart(@PathVariable String workflowName,
|
|
|
@RequestAttribute(ExternalApiAuthFilter.ATTR_KEY_ENTRY) ExternalApiProperties.KeyEntry keyEntry,
|
|
|
@RequestParam(value = "payload", required = false) String payloadJson,
|
|
|
@RequestParam(value = "files", required = false) MultipartFile[] files,
|
|
|
@RequestHeader(value = "X-Relative-Path", required = false) List<String> relativePaths) {
|
|
|
CreateRunRequest req = parsePayload(payloadJson);
|
|
|
- return doCreateRun(workflowId, req, files, relativePaths);
|
|
|
+ return doCreateRun(workflowName, req, files, relativePaths);
|
|
|
}
|
|
|
|
|
|
- private ResponseEntity<?> doCreateRun(Long workflowId, CreateRunRequest req,
|
|
|
+ private ResponseEntity<?> doCreateRun(String workflowName, CreateRunRequest req,
|
|
|
MultipartFile[] files, List<String> relativePaths) {
|
|
|
- // 1. 工作流存在性 + 图结构校验
|
|
|
- Workflow wf = workflowRepository.findById(workflowId).orElse(null);
|
|
|
+ // 1. 工作流解析
|
|
|
+ Workflow wf = resolveWorkflow(workflowName);
|
|
|
if (wf == null) {
|
|
|
- return error(404, "WORKFLOW_NOT_FOUND", "工作流不存在: " + workflowId);
|
|
|
+ return error(404, "WORKFLOW_NOT_FOUND", "工作流不存在: " + workflowName);
|
|
|
}
|
|
|
if (wf.getGraphData() == null || wf.getGraphData().isBlank()
|
|
|
|| wf.getGraphData().equals("{\"nodes\":[],\"edges\":[]}")) {
|
|
|
@@ -136,7 +131,7 @@ public class ExternalApiController {
|
|
|
String runId;
|
|
|
try {
|
|
|
runId = generateRunId();
|
|
|
- Path runDir = runDirManager.createRunDir(workflowId, runId);
|
|
|
+ Path runDir = runDirManager.createRunDir(wf.getId(), wf.getName(), runId);
|
|
|
List<String> uploadedFiles = new ArrayList<>();
|
|
|
if (files != null) {
|
|
|
for (int i = 0; i < files.length; i++) {
|
|
|
@@ -160,30 +155,32 @@ public class ExternalApiController {
|
|
|
} catch (IllegalArgumentException e) {
|
|
|
return error(400, "VALIDATION_FAILED", e.getMessage());
|
|
|
} catch (IOException e) {
|
|
|
- log.error("[ExternalApi] 创建运行失败 workflowId={}", workflowId, e);
|
|
|
+ log.error("[ExternalApi] 创建运行失败 workflowName={}", workflowName, e);
|
|
|
return error(500, "INTERNAL_ERROR", "工作目录创建失败");
|
|
|
}
|
|
|
|
|
|
- // 4. 注册 CREATED 状态 + 在 EventBus 占位(确保订阅时一定能找到)
|
|
|
+ // 4. 注册 CREATED 状态
|
|
|
int ttlHours = (req != null && req.getTtlHours() != null && req.getTtlHours() > 0)
|
|
|
? req.getTtlHours() : DEFAULT_TTL_HOURS;
|
|
|
- runRegistry.register(runId, workflowId, inputs, ttlHours);
|
|
|
+ runRegistry.register(runId, wf.getId(), wf.getName(), inputs, ttlHours);
|
|
|
eventBus.register(runId);
|
|
|
|
|
|
- // 5. async=false:立即启动并直接进入 SSE 流(与现有 /run 行为一致)
|
|
|
+ // 5. async=false:立即启动并直接进入 SSE 流
|
|
|
boolean async = req == null || req.getAsync() == null || req.getAsync();
|
|
|
if (!async) {
|
|
|
- return startRun(workflowId, runId, inputs);
|
|
|
+ return startRun(wf.getName(), wf.getId(), runId, inputs);
|
|
|
}
|
|
|
|
|
|
- // 6. async=true:返回 runId + links
|
|
|
+ // 6. async=true:返回 runId + links(URL 使用 workflowName)
|
|
|
CreateRunResponse body = new CreateRunResponse();
|
|
|
body.setRunId(runId);
|
|
|
- body.setWorkflowId(workflowId);
|
|
|
+ body.setWorkflowId(wf.getId());
|
|
|
+ body.setWorkflowName(wf.getName());
|
|
|
+ body.setDisplayName(wf.getDisplayName());
|
|
|
body.setStatus("CREATED");
|
|
|
body.setCreatedAt(Instant.now());
|
|
|
Links links = new Links();
|
|
|
- String base = "/api/v1/workflows/" + workflowId + "/runs/" + runId;
|
|
|
+ String base = "/api/v1/workflows/" + wf.getName() + "/runs/" + runId;
|
|
|
links.setStart(base + "/start");
|
|
|
links.setStream(base + "/stream");
|
|
|
links.setResult(base);
|
|
|
@@ -193,48 +190,46 @@ public class ExternalApiController {
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // POST /workflows/{id}/runs/{runId}/start — 启动运行
|
|
|
+ // POST /workflows/{workflowName}/runs/{runId}/start — 启动运行
|
|
|
// ============================================================
|
|
|
|
|
|
- @PostMapping("/workflows/{workflowId}/runs/{runId}/start")
|
|
|
- public ResponseEntity<?> startRun(@PathVariable Long workflowId,
|
|
|
+ @PostMapping("/workflows/{workflowName}/runs/{runId}/start")
|
|
|
+ public ResponseEntity<?> startRun(@PathVariable String workflowName,
|
|
|
@PathVariable String runId) {
|
|
|
ExternalRunRegistry.CreatedRun created = runRegistry.consume(runId);
|
|
|
if (created == null) {
|
|
|
return error(404, "RUN_NOT_FOUND", "运行不存在或已启动: " + runId);
|
|
|
}
|
|
|
- if (!Objects.equals(created.workflowId(), workflowId)) {
|
|
|
- return error(400, "VALIDATION_FAILED", "runId 与 workflowId 不匹配");
|
|
|
+ // 校验 pathVariable 与 created 一致(兼容数字字符串)
|
|
|
+ if (!matchesWorkflow(created, workflowName)) {
|
|
|
+ return error(400, "VALIDATION_FAILED", "runId 与 workflowName 不匹配");
|
|
|
}
|
|
|
- return startRun(workflowId, runId, created.inputs());
|
|
|
+ return startRun(created.workflowName(), created.workflowId(), runId, created.inputs());
|
|
|
}
|
|
|
|
|
|
- private ResponseEntity<?> startRun(Long workflowId, String runId, Map<String, Object> inputs) {
|
|
|
- // executeForExternal 直接以 runId 启动执行,不走 _preRunId 机制(避免污染 variables)
|
|
|
+ private ResponseEntity<?> startRun(String workflowName, Long workflowId, String runId, Map<String, Object> inputs) {
|
|
|
workflowEngine.executeForExternal(workflowId, runId, inputs);
|
|
|
|
|
|
StartRunResponse body = new StartRunResponse();
|
|
|
body.setRunId(runId);
|
|
|
body.setStatus("RUNNING");
|
|
|
- body.setStreamUrl("/api/v1/workflows/" + workflowId + "/runs/" + runId + "/stream");
|
|
|
+ body.setStreamUrl("/api/v1/workflows/" + workflowName + "/runs/" + runId + "/stream");
|
|
|
return ResponseEntity.ok(ApiError.ok(body));
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // GET /workflows/{id}/runs/{runId}/stream — SSE 流(含 Last-Event-ID 重连)
|
|
|
+ // GET /workflows/{workflowName}/runs/{runId}/stream — SSE 流
|
|
|
// ============================================================
|
|
|
|
|
|
- @GetMapping(value = "/workflows/{workflowId}/runs/{runId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
|
- public ResponseEntity<SseEmitter> streamRun(@PathVariable Long workflowId,
|
|
|
+ @GetMapping(value = "/workflows/{workflowName}/runs/{runId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
|
|
+ public ResponseEntity<SseEmitter> streamRun(@PathVariable String workflowName,
|
|
|
@PathVariable String runId,
|
|
|
@RequestHeader(value = "Last-Event-ID", required = false) Long lastEventId) {
|
|
|
- // runId 必须存在于 EventBus(CREATED 或 RUNNING)或运行历史
|
|
|
if (!eventBus.exists(runId)) {
|
|
|
WorkflowRun record = findRunRecord(runId);
|
|
|
if (record == null) {
|
|
|
return ResponseEntity.notFound().build();
|
|
|
}
|
|
|
- // 已结束的运行:注册一个已完成 stream,subscribe 后只补发缓存
|
|
|
eventBus.register(runId);
|
|
|
eventBus.publish(runId,
|
|
|
record.getStatus().equals("FAILED") ? "workflow_error" : "workflow_complete",
|
|
|
@@ -246,32 +241,35 @@ public class ExternalApiController {
|
|
|
return ResponseEntity.notFound().build();
|
|
|
}
|
|
|
return ResponseEntity.ok()
|
|
|
- .header("X-Accel-Buffering", "no") // 禁用 Nginx 缓冲,确保实时推送
|
|
|
+ .header("X-Accel-Buffering", "no")
|
|
|
.header("Cache-Control", "no-cache")
|
|
|
.body(emitter);
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // GET /workflows/{id}/runs/{runId} — 查询运行结果
|
|
|
+ // GET /workflows/{workflowName}/runs/{runId} — 查询运行结果
|
|
|
// ============================================================
|
|
|
|
|
|
- @GetMapping("/workflows/{workflowId}/runs/{runId}")
|
|
|
- public ResponseEntity<?> getRunResult(@PathVariable Long workflowId,
|
|
|
+ @GetMapping("/workflows/{workflowName}/runs/{runId}")
|
|
|
+ public ResponseEntity<?> getRunResult(@PathVariable String workflowName,
|
|
|
@PathVariable String runId) {
|
|
|
+ Workflow wf = resolveWorkflow(workflowName);
|
|
|
+ if (wf == null) {
|
|
|
+ return error(404, "WORKFLOW_NOT_FOUND", "工作流不存在: " + workflowName);
|
|
|
+ }
|
|
|
RunResultResponse body = new RunResultResponse();
|
|
|
body.setRunId(runId);
|
|
|
- body.setWorkflowId(workflowId);
|
|
|
+ body.setWorkflowId(wf.getId());
|
|
|
+ body.setWorkflowName(wf.getName());
|
|
|
+ body.setDisplayName(wf.getDisplayName());
|
|
|
|
|
|
- // 1. 优先从 EventBus 取实时状态
|
|
|
SseEventBus.RunStatus live = eventBus.getStatus(runId);
|
|
|
if (live != null) {
|
|
|
body.setStatus(live.status());
|
|
|
}
|
|
|
|
|
|
- // 2. 从数据库取持久化结果(RUNNING 时 outputs 为空)
|
|
|
WorkflowRun record = findRunRecord(runId);
|
|
|
if (record == null) {
|
|
|
- // 仍在 CREATED 状态
|
|
|
ExternalRunRegistry.CreatedRun created = runRegistry.peek(runId);
|
|
|
if (created != null) {
|
|
|
body.setStatus("CREATED");
|
|
|
@@ -290,7 +288,6 @@ public class ExternalApiController {
|
|
|
body.setError(record.getError());
|
|
|
body.setOutputs(parseOutputs(record));
|
|
|
|
|
|
- // 3. 节点列表
|
|
|
List<WorkflowRunNode> nodes = workflowRunNodeRepository.findByRunIdOrderBySortOrderAsc(record.getId());
|
|
|
List<NodeResult> nodeDtos = nodes.stream().map(n -> {
|
|
|
NodeResult dto = new NodeResult();
|
|
|
@@ -301,13 +298,12 @@ public class ExternalApiController {
|
|
|
dto.setOutput(parseJsonToMap(n.getOutput()));
|
|
|
dto.setError(n.getError());
|
|
|
dto.setLogsCount(countLogs(n.getLogs()));
|
|
|
- dto.setLogsUrl("/api/v1/workflows/" + workflowId + "/runs/" + runId + "/nodes/" + n.getNodeId() + "/logs");
|
|
|
+ dto.setLogsUrl("/api/v1/workflows/" + wf.getName() + "/runs/" + runId + "/nodes/" + n.getNodeId() + "/logs");
|
|
|
return dto;
|
|
|
}).collect(Collectors.toList());
|
|
|
body.setNodes(nodeDtos);
|
|
|
|
|
|
- // 4. 工作空间信息
|
|
|
- Path runDir = runDirManager.getRunDir(workflowId, runId);
|
|
|
+ Path runDir = runDirManager.getRunDir(wf.getId(), wf.getName(), runId);
|
|
|
if (runDir != null) {
|
|
|
try {
|
|
|
final int[] fileCount = {0};
|
|
|
@@ -321,7 +317,7 @@ public class ExternalApiController {
|
|
|
WorkspaceInfo ws = new WorkspaceInfo();
|
|
|
ws.setFileCount(fileCount[0]);
|
|
|
ws.setTotalBytes(totalBytes[0]);
|
|
|
- ws.setDownloadUrl("/api/v1/workflows/" + workflowId + "/runs/" + runId + "/workspace");
|
|
|
+ ws.setDownloadUrl("/api/v1/workflows/" + wf.getName() + "/runs/" + runId + "/workspace");
|
|
|
body.setWorkspace(ws);
|
|
|
} catch (IOException e) {
|
|
|
log.warn("[ExternalApi] 统计工作目录失败 runId={}: {}", runId, e.getMessage());
|
|
|
@@ -332,13 +328,17 @@ public class ExternalApiController {
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // GET /workflows/{id}/runs/{runId}/workspace — 下载工作空间 zip
|
|
|
+ // GET /workflows/{workflowName}/runs/{runId}/workspace
|
|
|
// ============================================================
|
|
|
|
|
|
- @GetMapping("/workflows/{workflowId}/runs/{runId}/workspace")
|
|
|
- public ResponseEntity<Resource> downloadWorkspace(@PathVariable Long workflowId,
|
|
|
+ @GetMapping("/workflows/{workflowName}/runs/{runId}/workspace")
|
|
|
+ public ResponseEntity<Resource> downloadWorkspace(@PathVariable String workflowName,
|
|
|
@PathVariable String runId) throws IOException {
|
|
|
- Path runDir = runDirManager.getRunDir(workflowId, runId);
|
|
|
+ Workflow wf = resolveWorkflow(workflowName);
|
|
|
+ if (wf == null) {
|
|
|
+ return ResponseEntity.notFound().build();
|
|
|
+ }
|
|
|
+ Path runDir = runDirManager.getRunDir(wf.getId(), wf.getName(), runId);
|
|
|
if (runDir == null) {
|
|
|
return ResponseEntity.notFound().build();
|
|
|
}
|
|
|
@@ -360,7 +360,7 @@ public class ExternalApiController {
|
|
|
}
|
|
|
|
|
|
Resource resource = new UrlResource(zipPath.toUri());
|
|
|
- String filename = URLEncoder.encode("workflow-" + workflowId + "-run-" + runId + ".zip", StandardCharsets.UTF_8);
|
|
|
+ String filename = URLEncoder.encode("workflow-" + wf.getName() + "-run-" + runId + ".zip", StandardCharsets.UTF_8);
|
|
|
return ResponseEntity.ok()
|
|
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + filename)
|
|
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
|
|
@@ -368,11 +368,11 @@ public class ExternalApiController {
|
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
|
- // GET /workflows/{id}/runs/{runId}/nodes/{nodeId}/logs — 节点日志
|
|
|
+ // GET /workflows/{workflowName}/runs/{runId}/nodes/{nodeId}/logs
|
|
|
// ============================================================
|
|
|
|
|
|
- @GetMapping("/workflows/{workflowId}/runs/{runId}/nodes/{nodeId}/logs")
|
|
|
- public ResponseEntity<?> getNodeLogs(@PathVariable Long workflowId,
|
|
|
+ @GetMapping("/workflows/{workflowName}/runs/{runId}/nodes/{nodeId}/logs")
|
|
|
+ public ResponseEntity<?> getNodeLogs(@PathVariable String workflowName,
|
|
|
@PathVariable String runId,
|
|
|
@PathVariable String nodeId) {
|
|
|
WorkflowRun record = findRunRecord(runId);
|
|
|
@@ -421,9 +421,8 @@ public class ExternalApiController {
|
|
|
@GetMapping("/workflows")
|
|
|
public ResponseEntity<?> listWorkflows(@RequestAttribute(ExternalApiAuthFilter.ATTR_KEY_ENTRY) ExternalApiProperties.KeyEntry keyEntry) {
|
|
|
List<Workflow> all = workflowRepository.findAllByOrderByUpdatedAtDesc();
|
|
|
- // 过滤:API Key 白名单 + 必须包含 graphData
|
|
|
List<Workflow> accessible = all.stream()
|
|
|
- .filter(w -> properties.canAccess(keyEntry, w.getId()))
|
|
|
+ .filter(w -> properties.canAccess(keyEntry, w.getName(), w.getId()))
|
|
|
.filter(w -> w.getGraphData() != null && !w.getGraphData().isBlank())
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
@@ -432,6 +431,7 @@ public class ExternalApiController {
|
|
|
WorkflowSummary s = new WorkflowSummary();
|
|
|
s.setId(w.getId());
|
|
|
s.setName(w.getName());
|
|
|
+ s.setDisplayName(w.getDisplayName());
|
|
|
s.setDescription(w.getDescription());
|
|
|
try {
|
|
|
JsonNode root = MAPPER.readTree(w.getGraphData());
|
|
|
@@ -454,6 +454,28 @@ public class ExternalApiController {
|
|
|
// 工具方法
|
|
|
// ============================================================
|
|
|
|
|
|
+ /**
|
|
|
+ * 解析工作流:优先 findByName,回退 findById(兼容数字 id URL)。
|
|
|
+ */
|
|
|
+ private Workflow resolveWorkflow(String nameOrId) {
|
|
|
+ if (nameOrId == null || nameOrId.isEmpty()) return null;
|
|
|
+ return workflowRepository.findByName(nameOrId)
|
|
|
+ .orElseGet(() -> {
|
|
|
+ if (nameOrId.matches("\\d+")) {
|
|
|
+ return workflowRepository.findById(Long.valueOf(nameOrId)).orElse(null);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 判断 created run 是否与 pathVariable workflowName 匹配(name 或数字 id 兼容) */
|
|
|
+ private boolean matchesWorkflow(ExternalRunRegistry.CreatedRun created, String workflowName) {
|
|
|
+ if (created == null || workflowName == null) return false;
|
|
|
+ if (workflowName.equals(created.workflowName())) return true;
|
|
|
+ if (workflowName.matches("\\d+") && Long.valueOf(workflowName).equals(created.workflowId())) return true;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
private ResponseEntity<ApiError> error(int httpStatus, String code, String message) {
|
|
|
return ResponseEntity.status(httpStatus).body(ApiError.error(code, message));
|
|
|
}
|
|
|
@@ -472,7 +494,6 @@ public class ExternalApiController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 根据字符串 runId 查找数据库 WorkflowRun(unique 索引) */
|
|
|
private WorkflowRun findRunRecord(String runId) {
|
|
|
return workflowRunRepository.findAllByOrderByStartedAtDesc().stream()
|
|
|
.filter(r -> runId.equals(r.getRunId()))
|
|
|
@@ -508,7 +529,6 @@ public class ExternalApiController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 从 graphData 提取某类型节点的字段定义 */
|
|
|
private List<FieldDef> extractFields(JsonNode root, String nodeType) {
|
|
|
List<FieldDef> result = new ArrayList<>();
|
|
|
JsonNode nodes = root.path("nodes");
|
|
|
@@ -516,15 +536,12 @@ public class ExternalApiController {
|
|
|
for (JsonNode node : nodes) {
|
|
|
if (!nodeType.equals(node.path("type").asText(""))) continue;
|
|
|
JsonNode data = node.path("data");
|
|
|
-
|
|
|
- // userInput 节点字段定义在 data.variables
|
|
|
JsonNode varArr = data.path("variables");
|
|
|
if (varArr.isArray()) {
|
|
|
for (JsonNode v : varArr) {
|
|
|
result.add(toFieldDef(v));
|
|
|
}
|
|
|
}
|
|
|
- // output 节点字段定义在 data.fields
|
|
|
JsonNode fieldArr = data.path("fields");
|
|
|
if (fieldArr.isArray()) {
|
|
|
for (JsonNode f : fieldArr) {
|
|
|
@@ -544,7 +561,6 @@ public class ExternalApiController {
|
|
|
return f;
|
|
|
}
|
|
|
|
|
|
- /** zip 打包过滤:应用 .agentignore 规则(含祖先目录) */
|
|
|
private boolean shouldIncludeInZip(Path runDir, Path p) {
|
|
|
Path rel = runDir.relativize(p);
|
|
|
String relStr = rel.toString().replace('\\', '/');
|