fix(team): pass lead attachments to workers (#612)

This commit is contained in:
matevip 2026-08-21 04:47:03 -04:00
parent 9ba188b4cb
commit 37ddcf185f
2 changed files with 88 additions and 1 deletions

View File

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

View File

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