新版本

This commit is contained in:
tanglong 2026-07-31 16:42:15 +08:00
parent 52d5bdacf3
commit e89fbd3141
5 changed files with 72 additions and 27 deletions

View File

@ -25,13 +25,13 @@
```json ```json
{ {
"query": "查看本月EC电话合规监控日报", "query": "查看本月EC电话合规监控日报"
"userId": "YiNianYiQingJing"
} }
``` ```
- `query` 同时兼容 `content`、`userText`、`message` 和 `input`。 - `query` 同时兼容 `content`、`userText`、`message` 和 `input`。
- `userId` 同时兼容 `wecomUserId`、`userid` 和 `user_id`。 - UserID 由后端从企业微信调用上下文请求头自动读取,不需要作为插件业务参数填写。
- 本地联调仍兼容请求体中的 `userId`、`wecomUserId`、`userid` 和 `user_id`。
- 处理逻辑保持不变:DeepSeek 语义识别 → 日期解析 → UserID 数据权限 → 对应日报处理器。 - 处理逻辑保持不变:DeepSeek 语义识别 → 日期解析 → UserID 数据权限 → 对应日报处理器。
- 同一个 UserID 同时只能执行一条运动馆指令。 - 同一个 UserID 同时只能执行一条运动馆指令。

View File

@ -4,6 +4,7 @@ import com.company.agentfoundation.integration.coze.service.CozeWorkflowService;
import com.company.agentfoundation.semantic.SemanticDispatchResult; import com.company.agentfoundation.semantic.SemanticDispatchResult;
import com.company.agentfoundation.semantic.SemanticDispatchService; import com.company.agentfoundation.semantic.SemanticDispatchService;
import com.company.agentfoundation.semantic.SemanticReplyAttachment; import com.company.agentfoundation.semantic.SemanticReplyAttachment;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ContentDisposition; import org.springframework.http.ContentDisposition;
@ -57,11 +58,17 @@ public class AssistantApiController {
} }
@PostMapping("/sports-venue/query") @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 query = request == null ? "" : request.normalizedQuery();
String userId = request == null ? "" : request.normalizedUserId(); String userId = resolveUserId(request, httpRequest);
if (query.isEmpty()) return AssistantQueryResponse.failure("query不能为空"); 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); String runningKey = userId.toLowerCase(Locale.ROOT);
if (!runningUsers.add(runningKey)) { if (!runningUsers.add(runningKey)) {
@ -121,4 +128,28 @@ public class AssistantApiController {
} }
return content.toString(); 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();
}
} }

View File

@ -3,7 +3,6 @@ package com.company.agentfoundation.assistant.api;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -18,8 +17,7 @@ public class AssistantOpenApiController {
"问答小助手API", "问答小助手API",
"/api/assistant/general/query", "/api/assistant/general/query",
"generalAssistantQuery", "generalAssistantQuery",
"将用户输入发送给扣子问答工作流并返回回答", "将用户输入发送给扣子问答工作流并返回回答");
false);
} }
@GetMapping({"/sports-venue", "/sports-venue.json"}) @GetMapping({"/sports-venue", "/sports-venue.json"})
@ -28,32 +26,26 @@ public class AssistantOpenApiController {
"运动馆智能助手API", "运动馆智能助手API",
"/api/assistant/sports-venue/query", "/api/assistant/sports-venue/query",
"sportsVenueAssistantQuery", "sportsVenueAssistantQuery",
"根据用户输入进行语义识别,并按企业微信UserID权限查询客资、学员、企业微信客户运营或EC日报", "根据用户输入进行语义识别,并按企业微信调用上下文中的UserID权限查询客资、学员、企业微信客户运营或EC日报");
true);
} }
private Map<String, Object> specification( private Map<String, Object> specification(
String title, String title,
String path, String path,
String operationId, String operationId,
String description, String description
boolean requireUserId
) { ) {
Map<String, Object> properties = new LinkedHashMap<>(); Map<String, Object> properties = new LinkedHashMap<>();
properties.put("query", Map.of( properties.put("query", Map.of(
"type", "string", "type", "string",
"description", "用户在企业微信中输入的完整问题或指令", "description", "用户在企业微信中输入的完整问题或指令",
"example", requireUserId ? "查看今日客资管理日报" : "测试表")); "example", operationId.equals("sportsVenueAssistantQuery")
if (requireUserId) { ? "查看今日客资管理日报"
properties.put("userId", Map.of( : "测试表"));
"type", "string",
"description", "当前提问人的企业微信UserID,用于数据权限判断",
"example", "YiNianYiQingJing"));
}
Map<String, Object> requestSchema = new LinkedHashMap<>(); Map<String, Object> requestSchema = new LinkedHashMap<>();
requestSchema.put("type", "object"); 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); requestSchema.put("properties", properties);
Map<String, Object> attachmentProperties = Map.of( Map<String, Object> attachmentProperties = Map.of(
@ -86,11 +78,9 @@ public class AssistantOpenApiController {
"description", "处理结果", "description", "处理结果",
"content", Map.of("application/json", Map.of("schema", responseSchema))))); "content", Map.of("application/json", Map.of("schema", responseSchema)))));
String server = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
return Map.of( return Map.of(
"openapi", "3.0.1", "openapi", "3.0.1",
"info", Map.of("title", title, "version", "1.0.0", "description", description), "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))); "paths", Map.of(path, Map.of("post", operation)));
} }
} }

View File

@ -73,7 +73,27 @@ class AssistantApiControllerTest {
} }
@Test @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( MockMvc mvc = mvc(
mock(CozeWorkflowService.class), mock(CozeWorkflowService.class),
mock(SemanticDispatchService.class), mock(SemanticDispatchService.class),
@ -84,7 +104,7 @@ class AssistantApiControllerTest {
.content("{\"query\":\"查看今日客资日报\"}")) .content("{\"query\":\"查看今日客资日报\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(false)) .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( private MockMvc mvc(

View File

@ -31,7 +31,11 @@ class AssistantOpenApiControllerTest {
.andExpect(jsonPath("$.paths['/api/assistant/sports-venue/query'].post.operationId") .andExpect(jsonPath("$.paths['/api/assistant/sports-venue/query'].post.operationId")
.value("sportsVenueAssistantQuery")) .value("sportsVenueAssistantQuery"))
.andExpect(jsonPath( .andExpect(jsonPath(
"$.paths['/api/assistant/sports-venue/query'].post.requestBody.content['application/json'].schema.required[1]") "$.paths['/api/assistant/sports-venue/query'].post.requestBody.content['application/json'].schema.required.length()")
.value("userId")); .value(1))
.andExpect(jsonPath(
"$.paths['/api/assistant/sports-venue/query'].post.requestBody.content['application/json'].schema.properties.userId")
.doesNotExist())
.andExpect(jsonPath("$.servers").doesNotExist());
} }
} }