fix(team): attach generated deliverables from worker replies (#606)

This commit is contained in:
matevip 2026-08-20 02:11:48 -04:00
parent d1a553ed77
commit e9b3c1697f
2 changed files with 58 additions and 3 deletions

View File

@ -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

View File

@ -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() {