新版本

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
{
"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 同时只能执行一条运动馆指令。

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.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();
}
}

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.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<String, Object> specification(
String title,
String path,
String operationId,
String description,
boolean requireUserId
String description
) {
Map<String, Object> 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<String, Object> 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<String, Object> 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)));
}
}

View File

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

View File

@ -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());
}
}