diff --git a/src/main/java/com/company/agentfoundation/assistant/api/AssistantApiController.java b/src/main/java/com/company/agentfoundation/assistant/api/AssistantApiController.java index 4ef6a72..eedd1d9 100644 --- a/src/main/java/com/company/agentfoundation/assistant/api/AssistantApiController.java +++ b/src/main/java/com/company/agentfoundation/assistant/api/AssistantApiController.java @@ -7,6 +7,7 @@ import com.company.agentfoundation.semantic.SemanticReplyAttachment; import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -26,6 +27,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; @RestController @RequestMapping("/api/assistant") @@ -35,16 +37,20 @@ public class AssistantApiController { private final CozeWorkflowService cozeWorkflowService; private final SemanticDispatchService semanticDispatchService; private final AssistantFileStore fileStore; + private final Executor assistantTaskExecutor; + private final Set runningGeneralWrites = ConcurrentHashMap.newKeySet(); private final Set runningUsers = ConcurrentHashMap.newKeySet(); public AssistantApiController( CozeWorkflowService cozeWorkflowService, SemanticDispatchService semanticDispatchService, - AssistantFileStore fileStore + AssistantFileStore fileStore, + @Qualifier("wecomTaskExecutor") Executor assistantTaskExecutor ) { this.cozeWorkflowService = cozeWorkflowService; this.semanticDispatchService = semanticDispatchService; this.fileStore = fileStore; + this.assistantTaskExecutor = assistantTaskExecutor; } @PostMapping( @@ -54,6 +60,9 @@ public class AssistantApiController { public AssistantQueryResponse generalQuery(@RequestBody AssistantQueryRequest request) { String query = request == null ? "" : request.normalizedQuery(); if (query.isEmpty()) return AssistantQueryResponse.failure("query不能为空"); + if (isCustomerCreation(query)) { + return acceptCustomerCreation(query); + } try { return AssistantQueryResponse.success( toRobotPlainText(cozeWorkflowService.chatDefault(query)), List.of()); @@ -63,6 +72,46 @@ public class AssistantApiController { } } + private AssistantQueryResponse acceptCustomerCreation(String query) { + String requestKey = query.toLowerCase(Locale.ROOT).replaceAll("\\s+", ""); + if (!runningGeneralWrites.add(requestKey)) { + return AssistantQueryResponse.success( + "相同客户新增请求正在处理中,请勿重复提交。", List.of()); + } + try { + assistantTaskExecutor.execute(() -> { + try { + String reply = toRobotPlainText(cozeWorkflowService.chatDefault(query)); + log.info("assistant general async customer creation completed query={} reply={}", + query, reply); + } catch (RuntimeException exception) { + log.error("assistant general async customer creation failed query={} err={}", + query, exception.getMessage(), exception); + } finally { + runningGeneralWrites.remove(requestKey); + } + }); + return AssistantQueryResponse.success( + "客户新增请求已受理,正在后台处理,请勿重复提交。", List.of()); + } catch (RuntimeException exception) { + runningGeneralWrites.remove(requestKey); + log.error("assistant general async customer creation rejected query={} err={}", + query, exception.getMessage(), exception); + return AssistantQueryResponse.failure("客户新增请求提交失败,请稍后重试。"); + } + } + + private boolean isCustomerCreation(String query) { + String compact = query == null ? "" : query.replaceAll("\\s+", ""); + boolean hasCreationVerb = compact.contains("新增") + || compact.contains("新建") + || compact.contains("添加") + || compact.contains("创建") + || compact.contains("录入"); + boolean hasCustomerObject = compact.contains("客户") || compact.contains("客资"); + return hasCreationVerb && hasCustomerObject; + } + @PostMapping( value = { "/sports-venue/query", diff --git a/src/test/java/com/company/agentfoundation/assistant/api/AssistantApiControllerTest.java b/src/test/java/com/company/agentfoundation/assistant/api/AssistantApiControllerTest.java index adfbc23..c8beaac 100644 --- a/src/test/java/com/company/agentfoundation/assistant/api/AssistantApiControllerTest.java +++ b/src/test/java/com/company/agentfoundation/assistant/api/AssistantApiControllerTest.java @@ -13,6 +13,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.List; +import java.util.concurrent.Executor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -40,6 +41,24 @@ class AssistantApiControllerTest { verify(coze).chatDefault("测试表"); } + @Test + void generalApiAcceptsCustomerCreationAsynchronously() throws Exception { + CozeWorkflowService coze = mock(CozeWorkflowService.class); + CapturingExecutor executor = new CapturingExecutor(); + MockMvc mvc = mvc(coze, mock(SemanticDispatchService.class), new AssistantFileStore(), executor); + + mvc.perform(post("/api/assistant/general/query") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"query\":\"新增客户 名称 张三 电话 13632363654\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("已受理"))); + + org.mockito.Mockito.verifyNoInteractions(coze); + executor.runCaptured(); + verify(coze).chatDefault("新增客户 名称 张三 电话 13632363654"); + } + @Test void sportsVenueApiRunsSemanticRoutingAndReturnsDownloadableExcel() throws Exception { SemanticDispatchService semantic = mock(SemanticDispatchService.class); @@ -185,10 +204,34 @@ class AssistantApiControllerTest { SemanticDispatchService semantic, AssistantFileStore files ) { - return MockMvcBuilders.standaloneSetup(new AssistantApiController(coze, semantic, files)) + return mvc(coze, semantic, files, Runnable::run); + } + + private MockMvc mvc( + CozeWorkflowService coze, + SemanticDispatchService semantic, + AssistantFileStore files, + Executor executor + ) { + return MockMvcBuilders.standaloneSetup( + new AssistantApiController(coze, semantic, files, executor)) .setMessageConverters( new MappingJackson2HttpMessageConverter(), new ByteArrayHttpMessageConverter()) .build(); } + + private static final class CapturingExecutor implements Executor { + private Runnable captured; + + @Override + public void execute(Runnable command) { + captured = command; + } + + void runCaptured() { + if (captured == null) throw new AssertionError("No asynchronous task was submitted"); + captured.run(); + } + } }