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 0974d536..3cc6a299 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 @@ -57,6 +57,26 @@ public class TeamDispatchService { private static final Pattern GENERATED_FILE_MARKDOWN_LINK = Pattern.compile( "\\[([^\\]\\r\\n]{1,200})]\\(((?:https?://[^/\\s)\\]]+)?/api/v1/files/generated/[A-Za-z0-9-]+)\\)"); + private static final Set CLARIFICATION_CUES = Set.of( + "您希望", + "你希望", + "是否继续", + "请问", + "能否", + "可以提供", + "请提供", + "需要您", + "需要你", + "我应该", + "如何处理", + "what would you like", + "how should i", + "could you provide", + "please provide", + "do you want me", + "should i", + "would you like"); + /** One JDK 21 virtual thread per member-agent run. */ private static final ExecutorService DISPATCH_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor(); @@ -337,9 +357,37 @@ public class TeamDispatchService { && taskService.listDeliverables(task).isEmpty()) { return "required deliverable was not attached"; } + if (looksLikeClarificationQuestion(reply)) { + return "member asked for clarification instead of producing a result"; + } return null; } + private boolean looksLikeClarificationQuestion(String reply) { + if (reply == null) { + return false; + } + String normalized = reply.strip().replaceAll("\\s+", " "); + if (normalized.isBlank() || normalized.length() > 800) { + return false; + } + String lower = normalized.toLowerCase(); + boolean hasCue = CLARIFICATION_CUES.stream().anyMatch(lower::contains); + if (!hasCue) { + return false; + } + return normalized.endsWith("?") + || normalized.endsWith("?") + || lower.contains("what would you like") + || lower.contains("how should i") + || lower.contains("could you provide") + || lower.contains("please provide") + || normalized.contains("请问") + || normalized.contains("是否继续") + || normalized.contains("如何处理") + || normalized.contains("请提供"); + } + private boolean attachGeneratedFileDeliverable(TeamTaskEntity task, String reply) { if (!requiresDeliverable(task) || reply == null || reply.isBlank()) { return false; @@ -393,7 +441,8 @@ public class TeamDispatchService { - Execute this task now. Your final reply becomes the task result reported to the team lead, so end with a complete, self-contained summary of what you produced. - Report milestones with team_tasks(action="progress", taskId=%s, percent=..., step=...). - If the output is a document, spreadsheet or presentation, generate a real file (renderDocx / renderXlsx / renderPptx or the docx/pptx/xlsx skills) and register it with team_tasks(action="attach", taskId=%s, name="", url=). Keep the result a summary — do not paste file contents. - - If you are missing an input you cannot obtain yourself, call team_tasks(action="comment", taskId=%s, type="blocker", text="what you need") and stop. + - If scope is ambiguous but you can make a reasonable assumption, state the assumption and continue. + - If you are missing an input you cannot obtain yourself, call team_tasks(action="comment", taskId=%s, type="blocker", text="what you need") and stop. Do not ask the lead or user for clarification in your final reply. """.formatted(task.getId(), task.getId(), task.getId())); return sb.toString(); } 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 f3e9309c..22b0afcf 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 @@ -173,6 +173,26 @@ class TeamDispatchServiceTest { verify(eventChannel).publishTaskEvent(any(), eq("team_task_retrying"), any()); } + @Test + @DisplayName("a clarifying question is requeued instead of being reported as completed") + void settleClarifyingQuestionRequeues() { + TeamTaskEntity running = task(1L, MEMBER_A); + running.setStatus(TeamTaskStatus.IN_PROGRESS); + running.setDispatchCount(1); + when(taskService.getTask(1L)).thenReturn(running); + when(taskService.requeueUnusableResult(1L, + "member asked for clarification instead of producing a result")) + .thenReturn(true); + + service.settleOutcome(running, "您希望我如何处理?"); + + verify(taskService).requeueUnusableResult(1L, + "member asked for clarification instead of producing a result"); + verify(taskService, never()).completeTask(any(), any(), anyString()); + verify(announceService, never()).announceTaskSettled(any()); + verify(eventChannel).publishTaskEvent(any(), eq("team_task_retrying"), any()); + } + @Test @DisplayName("an unusable third result fails instead of bypassing the circuit breaker") void settleFallbackFailsAfterDispatchBudget() {