From e9b3c1697f09dd2548284c6d62ad12240182b65e Mon Sep 17 00:00:00 2001 From: matevip Date: Thu, 20 Aug 2026 02:11:48 -0400 Subject: [PATCH] fix(team): attach generated deliverables from worker replies (#606) --- .../team/service/TeamDispatchService.java | 39 +++++++++++++++++-- .../team/service/TeamDispatchServiceTest.java | 22 +++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/team/service/TeamDispatchService.java b/mateclaw-server/src/main/java/vip/mate/team/service/TeamDispatchService.java index c6dd34f4..0974d536 100644 --- a/mateclaw-server/src/main/java/vip/mate/team/service/TeamDispatchService.java +++ b/mateclaw-server/src/main/java/vip/mate/team/service/TeamDispatchService.java @@ -15,6 +15,7 @@ import vip.mate.team.model.AgentTeamEntity; import vip.mate.team.model.TeamTaskEntity; import vip.mate.team.model.TeamTaskEventEntity; import vip.mate.team.model.TeamTaskStatus; +import vip.mate.tool.document.GeneratedFileCache; import vip.mate.workspace.conversation.ConversationService; import java.util.HashMap; @@ -28,6 +29,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Dispatches board tasks to their assigned member agents and closes the @@ -51,6 +54,9 @@ public class TeamDispatchService { /** Result summaries are capped before persisting to keep the board readable. */ static final int MAX_RESULT_CHARS = 8000; + private static final Pattern GENERATED_FILE_MARKDOWN_LINK = Pattern.compile( + "\\[([^\\]\\r\\n]{1,200})]\\(((?:https?://[^/\\s)\\]]+)?/api/v1/files/generated/[A-Za-z0-9-]+)\\)"); + /** One JDK 21 virtual thread per member-agent run. */ private static final ExecutorService DISPATCH_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); @@ -248,7 +254,8 @@ public class TeamDispatchService { return; } if (TeamTaskStatus.IN_PROGRESS.equals(current.getStatus())) { - String invalidReason = invalidResultReason(current, reply); + boolean attachedGeneratedFile = attachGeneratedFileDeliverable(current, reply); + String invalidReason = invalidResultReason(current, reply, attachedGeneratedFile); if (invalidReason != null) { int attempts = current.getDispatchCount() == null ? 0 : current.getDispatchCount(); if (attempts < TeamTaskService.MAX_DISPATCHES @@ -316,7 +323,8 @@ public class TeamDispatchService { announceService.announceTaskSettled(current); } - private String invalidResultReason(TeamTaskEntity task, String reply) { + private String invalidResultReason(TeamTaskEntity task, String reply, + boolean attachedGeneratedFile) { if (reply == null || reply.isBlank()) { return "member produced no result"; } @@ -325,12 +333,37 @@ public class TeamDispatchService { || normalized.equals("(no output)")) { return "member response generation failed"; } - if (requiresDeliverable(task) && taskService.listDeliverables(task).isEmpty()) { + if (requiresDeliverable(task) && !attachedGeneratedFile + && taskService.listDeliverables(task).isEmpty()) { return "required deliverable was not attached"; } return null; } + private boolean attachGeneratedFileDeliverable(TeamTaskEntity task, String reply) { + if (!requiresDeliverable(task) || reply == null || reply.isBlank()) { + return false; + } + Matcher link = GENERATED_FILE_MARKDOWN_LINK.matcher(reply); + while (link.find()) { + String name = link.group(1).trim(); + String url = link.group(2).trim(); + if (!GeneratedFileCache.GENERATED_URL_PATTERN.matcher(url).matches()) { + continue; + } + try { + taskService.addDeliverable(task.getId(), task.getAssigneeAgentId(), name, url); + log.info("Team task #{} auto-attached generated deliverable from member reply: {}", + task.getTaskNumber(), name); + return true; + } catch (Exception e) { + log.warn("Team task #{} generated deliverable auto-attach failed for {}: {}", + task.getTaskNumber(), name, e.getMessage()); + } + } + return false; + } + private boolean requiresDeliverable(TeamTaskEntity task) { try { return task.getMetadata() != null diff --git a/mateclaw-server/src/test/java/vip/mate/team/service/TeamDispatchServiceTest.java b/mateclaw-server/src/test/java/vip/mate/team/service/TeamDispatchServiceTest.java index c20b313c..f3e9309c 100644 --- a/mateclaw-server/src/test/java/vip/mate/team/service/TeamDispatchServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/team/service/TeamDispatchServiceTest.java @@ -211,6 +211,28 @@ class TeamDispatchServiceTest { verify(taskService, never()).completeTask(any(), any(), anyString()); } + @Test + @DisplayName("a returnDirect generated-file link satisfies a declared deliverable task") + void settleGeneratedFileLinkAttachesDeliverable() { + TeamTaskEntity running = task(1L, MEMBER_A); + running.setStatus(TeamTaskStatus.IN_PROGRESS); + running.setMetadata("{\"deliverableRequired\":true}"); + TeamTaskEntity completed = task(1L, MEMBER_A); + completed.setStatus(TeamTaskStatus.COMPLETED); + when(taskService.getTask(1L)).thenReturn(running, completed); + when(taskService.listDeliverables(running)).thenReturn(List.of()); + when(taskService.completeTask(eq(1L), isNull(), anyString())).thenReturn(List.of()); + + String reply = "文档已生成:[report.docx](/api/v1/files/generated/file-123)(链接 7 天内有效)。"; + service.settleOutcome(running, reply); + + verify(taskService).addDeliverable(1L, MEMBER_A, "report.docx", + "/api/v1/files/generated/file-123"); + verify(taskService, never()).requeueUnusableResult(any(), anyString()); + verify(taskService).completeTask(1L, null, reply); + verify(announceService).announceTaskSettled(completed); + } + @Test @DisplayName("a long-running checkpoint tracker stays active until its terminal round") void settleParksCheckpointTracker() {