From e89fbd3141f157bccd48ef72e51b19149723a138 Mon Sep 17 00:00:00 2001 From: tanglong <842690096@qq.com> Date: Fri, 31 Jul 2026 16:42:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/assistant-api.md | 6 +-- .../assistant/api/AssistantApiController.java | 37 +++++++++++++++++-- .../api/AssistantOpenApiController.java | 24 ++++-------- .../api/AssistantApiControllerTest.java | 24 +++++++++++- .../api/AssistantOpenApiControllerTest.java | 8 +++- 5 files changed, 72 insertions(+), 27 deletions(-) diff --git a/docs/assistant-api.md b/docs/assistant-api.md index 2711274..2220c8c 100644 --- a/docs/assistant-api.md +++ b/docs/assistant-api.md @@ -25,13 +25,13 @@ ```json { - "query": "查看本月EC电话合规监控日报", - "userId": "YiNianYiQingJing" + "query": "查看本月EC电话合规监控日报" } ``` - `query` 同时兼容 `content`、`userText`、`message` 和 `input`。 -- `userId` 同时兼容 `wecomUserId`、`userid` 和 `user_id`。 +- UserID 由后端从企业微信调用上下文请求头自动读取,不需要作为插件业务参数填写。 +- 本地联调仍兼容请求体中的 `userId`、`wecomUserId`、`userid` 和 `user_id`。 - 处理逻辑保持不变:DeepSeek 语义识别 → 日期解析 → UserID 数据权限 → 对应日报处理器。 - 同一个 UserID 同时只能执行一条运动馆指令。 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 323121c..8a5a7c1 100644 --- a/src/main/java/com/company/agentfoundation/assistant/api/AssistantApiController.java +++ b/src/main/java/com/company/agentfoundation/assistant/api/AssistantApiController.java @@ -4,6 +4,7 @@ import com.company.agentfoundation.integration.coze.service.CozeWorkflowService; import com.company.agentfoundation.semantic.SemanticDispatchResult; import com.company.agentfoundation.semantic.SemanticDispatchService; import com.company.agentfoundation.semantic.SemanticReplyAttachment; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ContentDisposition; @@ -57,11 +58,17 @@ public class AssistantApiController { } @PostMapping("/sports-venue/query") - public AssistantQueryResponse sportsVenueQuery(@RequestBody AssistantQueryRequest request) { + public AssistantQueryResponse sportsVenueQuery( + @RequestBody AssistantQueryRequest request, + HttpServletRequest httpRequest + ) { String query = request == null ? "" : request.normalizedQuery(); - String userId = request == null ? "" : request.normalizedUserId(); + String userId = resolveUserId(request, httpRequest); if (query.isEmpty()) return AssistantQueryResponse.failure("query不能为空"); - if (userId.isEmpty()) return AssistantQueryResponse.failure("userId不能为空,数据权限需要企业微信UserID"); + if (userId.isEmpty()) { + return AssistantQueryResponse.failure( + "未能从企业微信调用上下文获取UserID,暂时无法判断数据权限。"); + } String runningKey = userId.toLowerCase(Locale.ROOT); if (!runningUsers.add(runningKey)) { @@ -121,4 +128,28 @@ public class AssistantApiController { } return content.toString(); } + + private String resolveUserId( + AssistantQueryRequest request, + HttpServletRequest httpRequest + ) { + String[] headerNames = { + "X-WeCom-UserId", + "X-WeWork-UserId", + "X-WXWork-UserId", + "X-WX-UserId", + "X-User-Id", + "UserId" + }; + for (String headerName : headerNames) { + String value = httpRequest.getHeader(headerName); + if (value != null && !value.isBlank()) return value.trim(); + } + if (httpRequest.getUserPrincipal() != null + && httpRequest.getUserPrincipal().getName() != null + && !httpRequest.getUserPrincipal().getName().isBlank()) { + return httpRequest.getUserPrincipal().getName().trim(); + } + return request == null ? "" : request.normalizedUserId(); + } } diff --git a/src/main/java/com/company/agentfoundation/assistant/api/AssistantOpenApiController.java b/src/main/java/com/company/agentfoundation/assistant/api/AssistantOpenApiController.java index 60b8538..85c59ec 100644 --- a/src/main/java/com/company/agentfoundation/assistant/api/AssistantOpenApiController.java +++ b/src/main/java/com/company/agentfoundation/assistant/api/AssistantOpenApiController.java @@ -3,7 +3,6 @@ package com.company.agentfoundation.assistant.api; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.util.LinkedHashMap; import java.util.List; @@ -18,8 +17,7 @@ public class AssistantOpenApiController { "问答小助手API", "/api/assistant/general/query", "generalAssistantQuery", - "将用户输入发送给扣子问答工作流并返回回答", - false); + "将用户输入发送给扣子问答工作流并返回回答"); } @GetMapping({"/sports-venue", "/sports-venue.json"}) @@ -28,32 +26,26 @@ public class AssistantOpenApiController { "运动馆智能助手API", "/api/assistant/sports-venue/query", "sportsVenueAssistantQuery", - "根据用户输入进行语义识别,并按企业微信UserID权限查询客资、学员、企业微信客户运营或EC日报", - true); + "根据用户输入进行语义识别,并按企业微信调用上下文中的UserID权限查询客资、学员、企业微信客户运营或EC日报"); } private Map specification( String title, String path, String operationId, - String description, - boolean requireUserId + String description ) { Map properties = new LinkedHashMap<>(); properties.put("query", Map.of( "type", "string", "description", "用户在企业微信中输入的完整问题或指令", - "example", requireUserId ? "查看今日客资管理日报" : "测试表")); - if (requireUserId) { - properties.put("userId", Map.of( - "type", "string", - "description", "当前提问人的企业微信UserID,用于数据权限判断", - "example", "YiNianYiQingJing")); - } + "example", operationId.equals("sportsVenueAssistantQuery") + ? "查看今日客资管理日报" + : "测试表")); Map requestSchema = new LinkedHashMap<>(); requestSchema.put("type", "object"); - requestSchema.put("required", requireUserId ? List.of("query", "userId") : List.of("query")); + requestSchema.put("required", List.of("query")); requestSchema.put("properties", properties); Map attachmentProperties = Map.of( @@ -86,11 +78,9 @@ public class AssistantOpenApiController { "description", "处理结果", "content", Map.of("application/json", Map.of("schema", responseSchema))))); - String server = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString(); return Map.of( "openapi", "3.0.1", "info", Map.of("title", title, "version", "1.0.0", "description", description), - "servers", List.of(Map.of("url", server)), "paths", Map.of(path, Map.of("post", operation))); } } 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 8c8465b..ec0c6d4 100644 --- a/src/test/java/com/company/agentfoundation/assistant/api/AssistantApiControllerTest.java +++ b/src/test/java/com/company/agentfoundation/assistant/api/AssistantApiControllerTest.java @@ -73,7 +73,27 @@ class AssistantApiControllerTest { } @Test - void sportsVenueApiRequiresUserIdForPermissionScope() throws Exception { + void sportsVenueApiReadsUserIdFromWecomContextHeader() throws Exception { + SemanticDispatchService semantic = mock(SemanticDispatchService.class); + when(semantic.dispatchWithAttachments("YiNianYiQingJing", "查看今日客资日报")) + .thenReturn(new SemanticDispatchResult("客资日报", List.of())); + MockMvc mvc = mvc( + mock(CozeWorkflowService.class), + semantic, + new AssistantFileStore()); + + mvc.perform(post("/api/assistant/sports-venue/query") + .header("X-WeCom-UserId", "YiNianYiQingJing") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"query\":\"查看今日客资日报\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + + verify(semantic).dispatchWithAttachments("YiNianYiQingJing", "查看今日客资日报"); + } + + @Test + void sportsVenueApiRejectsRequestWhenWecomContextHasNoUserId() throws Exception { MockMvc mvc = mvc( mock(CozeWorkflowService.class), mock(SemanticDispatchService.class), @@ -84,7 +104,7 @@ class AssistantApiControllerTest { .content("{\"query\":\"查看今日客资日报\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(false)) - .andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("userId"))); + .andExpect(jsonPath("$.content").value(org.hamcrest.Matchers.containsString("UserID"))); } private MockMvc mvc( diff --git a/src/test/java/com/company/agentfoundation/assistant/api/AssistantOpenApiControllerTest.java b/src/test/java/com/company/agentfoundation/assistant/api/AssistantOpenApiControllerTest.java index 30c8c44..2d1f2ce 100644 --- a/src/test/java/com/company/agentfoundation/assistant/api/AssistantOpenApiControllerTest.java +++ b/src/test/java/com/company/agentfoundation/assistant/api/AssistantOpenApiControllerTest.java @@ -31,7 +31,11 @@ class AssistantOpenApiControllerTest { .andExpect(jsonPath("$.paths['/api/assistant/sports-venue/query'].post.operationId") .value("sportsVenueAssistantQuery")) .andExpect(jsonPath( - "$.paths['/api/assistant/sports-venue/query'].post.requestBody.content['application/json'].schema.required[1]") - .value("userId")); + "$.paths['/api/assistant/sports-venue/query'].post.requestBody.content['application/json'].schema.required.length()") + .value(1)) + .andExpect(jsonPath( + "$.paths['/api/assistant/sports-venue/query'].post.requestBody.content['application/json'].schema.properties.userId") + .doesNotExist()) + .andExpect(jsonPath("$.servers").doesNotExist()); } }