From 37ddcf185fb228e9a6855b2b575755e8e87d5723 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 21 Aug 2026 04:47:03 -0400 Subject: [PATCH] fix(team): pass lead attachments to workers (#612) --- .../team/service/TeamDispatchService.java | 58 ++++++++++++++++++- .../team/service/TeamDispatchServiceTest.java | 31 ++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) 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 3cc6a299..e371bcdf 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 @@ -17,6 +17,7 @@ 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 vip.mate.workspace.conversation.model.MessageEntity; import java.util.HashMap; import java.util.HashSet; @@ -424,8 +425,11 @@ public class TeamDispatchService { /** Per-prerequisite and whole-section caps keeping the envelope bounded. */ static final int MAX_PREREQ_RESULT_CHARS = 1500; static final int MAX_PREREQ_SECTION_CHARS = 6000; + static final int LEAD_ATTACHMENT_CONTEXT_MESSAGES = 12; + static final int MAX_LEAD_ATTACHMENT_ITEM_CHARS = 1200; + static final int MAX_LEAD_ATTACHMENT_SECTION_CHARS = 6000; - /** The full instruction envelope the member receives; it cannot see the lead's conversation. */ + /** The full instruction envelope the member receives. */ private String buildDispatchContent(TeamTaskEntity task) { StringBuilder sb = new StringBuilder(1024); sb.append("[Assigned team task #").append(task.getTaskNumber()) @@ -434,6 +438,7 @@ public class TeamDispatchService { if (task.getDescription() != null && !task.getDescription().isBlank()) { sb.append("\n").append(task.getDescription()).append('\n'); } + appendLeadAttachmentContext(sb, task); appendPrerequisiteResults(sb, task); sb.append(""" @@ -447,6 +452,57 @@ public class TeamDispatchService { return sb.toString(); } + /** + * Child worker conversations are isolated from the lead transcript, so + * upload paths from the lead turn must be copied into the dispatch + * envelope explicitly. Only rendered attachment/media rows with local + * paths are included; ordinary lead chat text stays out of the member + * prompt. + */ + void appendLeadAttachmentContext(StringBuilder sb, TeamTaskEntity task) { + String leadConversationId = task.getLeadConversationId(); + if (leadConversationId == null || leadConversationId.isBlank()) { + return; + } + List messages = conversationService.listRecentMessages( + leadConversationId, LEAD_ATTACHMENT_CONTEXT_MESSAGES); + if (messages == null || messages.isEmpty()) { + return; + } + StringBuilder section = new StringBuilder(); + for (MessageEntity message : messages) { + if (message == null || message.getContentParts() == null + || message.getContentParts().isBlank()) { + continue; + } + String rendered = conversationService.renderMessageContent(message, true); + if (rendered == null || rendered.isBlank() || !hasRenderedAttachmentPath(rendered)) { + continue; + } + section.append("- ") + .append(truncate(rendered.strip(), MAX_LEAD_ATTACHMENT_ITEM_CHARS) + .replace("\n", "\n ")) + .append('\n'); + } + if (section.isEmpty()) { + return; + } + sb.append("\n[Lead conversation attachments]\n") + .append(truncate(section.toString(), MAX_LEAD_ATTACHMENT_SECTION_CHARS)) + .append("Use these paths when this task refers to files uploaded in the lead conversation.\n"); + } + + private static boolean hasRenderedAttachmentPath(String rendered) { + if (!rendered.contains("路径:")) { + return false; + } + return rendered.contains("[附件]") + || rendered.contains("[图片]") + || rendered.contains("[视频]") + || rendered.contains("[音频]") + || rendered.contains("[3D 模型]"); + } + /** * Hand the member everything its prerequisites produced: result summaries * and deliverable links, so upstream output flows downstream without the 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 22b0afcf..de5d6ade 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 @@ -10,6 +10,7 @@ import vip.mate.team.model.TeamTaskEntity; import vip.mate.team.model.TeamTaskCommentEntity; import vip.mate.team.model.TeamTaskStatus; import vip.mate.workspace.conversation.ConversationService; +import vip.mate.workspace.conversation.model.MessageEntity; import java.util.List; @@ -19,6 +20,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Mockito.*; /** @@ -357,6 +359,35 @@ class TeamDispatchServiceTest { eq("team_worker")); } + @Test + @DisplayName("member dispatch envelope carries lead conversation upload paths") + void runTaskCarriesLeadConversationUploadPaths() { + TeamTaskEntity assigned = task(1L, MEMBER_A); + assigned.setDescription("请根据刚上传的需求.docx拆解任务"); + assigned.setStatus(TeamTaskStatus.IN_PROGRESS); + TeamTaskEntity done = task(1L, MEMBER_A); + done.setStatus(TeamTaskStatus.COMPLETED); + MessageEntity uploadTurn = new MessageEntity(); + uploadTurn.setRole("user"); + uploadTurn.setContentParts("[{\"type\":\"file\"}]"); + when(conversationService.listRecentMessages("lead-conv", TeamDispatchService.LEAD_ATTACHMENT_CONTEXT_MESSAGES)) + .thenReturn(List.of(uploadTurn)); + when(conversationService.renderMessageContent(uploadTurn, true)) + .thenReturn("请看附件\n[附件] 需求.docx(路径: /workspace/uploads/lead-conv/需求.docx)"); + when(taskService.getTask(1L)).thenReturn(assigned, done, done); + when(taskService.completeTask(eq(1L), isNull(), anyString())).thenReturn(List.of()); + when(agentService.chatWithUsage(eq(MEMBER_A), anyString(), anyString())) + .thenReturn(AgentService.ChatResult.contentOnly("all done")); + + service.runTask(TEAM_ID, assigned); + + var prompt = forClass(String.class); + verify(agentService).chatWithUsage(eq(MEMBER_A), prompt.capture(), startsWith("team-task-")); + assertTrue(prompt.getValue().contains("[Lead conversation attachments]")); + assertTrue(prompt.getValue().contains("需求.docx")); + assertTrue(prompt.getValue().contains("/workspace/uploads/lead-conv/需求.docx")); + } + @Test @DisplayName("an interrupted run whose task was cancelled produces no failed event") void interruptedCancelledRunStaysSilent() {