fix: 完成系统向智能体数据链路切换
- 切换工作台、聊天历史、资源候选与公共调用到 Agent - 加固资源绑定、删除保护及发布运行并发控制 - 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
@@ -12,6 +12,10 @@
|
||||
<artifactId>easyflow-api-public</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
@@ -34,4 +38,4 @@
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.agent.runtime.AgentChatRequest;
|
||||
import tech.easyflow.agent.runtime.AgentRunService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
/**
|
||||
* Agent 公共调用接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/public-api/agent")
|
||||
public class PublicAgentController {
|
||||
|
||||
private final AgentRunService agentRunService;
|
||||
private final SysApiKeyService sysApiKeyService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 公共接口控制器。
|
||||
*
|
||||
* @param agentRunService Agent 运行服务
|
||||
* @param sysApiKeyService API Key 服务
|
||||
*/
|
||||
public PublicAgentController(AgentRunService agentRunService,
|
||||
SysApiKeyService sysApiKeyService) {
|
||||
this.agentRunService = agentRunService;
|
||||
this.sysApiKeyService = sysApiKeyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 API Key 调用已发布 Agent。
|
||||
*
|
||||
* @param chatRequest Agent 聊天请求
|
||||
* @param request HTTP 请求
|
||||
* @return SSE Emitter
|
||||
*/
|
||||
@PostMapping("/chat")
|
||||
public SseEmitter chat(@RequestBody AgentChatRequest chatRequest,
|
||||
HttpServletRequest request) {
|
||||
String apiKey = request.getHeader(SysApiKey.KEY_Apikey);
|
||||
if (!StringUtils.hasText(apiKey)) {
|
||||
throw new BusinessException(401, 401, "Apikey不能为空!");
|
||||
}
|
||||
sysApiKeyService.checkApikeyPermission(apiKey, request.getRequestURI());
|
||||
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||
return agentRunService.chatPublic(chatRequest, buildApiAccount(sysApiKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 API Key 转换为独立的聊天调用身份。
|
||||
*
|
||||
* @param sysApiKey API Key 记录
|
||||
* @return 调用身份
|
||||
*/
|
||||
private LoginAccount buildApiAccount(SysApiKey sysApiKey) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(sysApiKey.getId());
|
||||
account.setTenantId(sysApiKey.getTenantId() == null
|
||||
? java.math.BigInteger.ZERO
|
||||
: sysApiKey.getTenantId());
|
||||
account.setDeptId(sysApiKey.getDeptId() == null
|
||||
? java.math.BigInteger.ZERO
|
||||
: sysApiKey.getDeptId());
|
||||
account.setLoginName("apikey:" + sysApiKey.getId());
|
||||
account.setNickname("API 调用方");
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,10 @@ public class PublicApiConfig implements WebMvcConfigurer {
|
||||
|
||||
registry.addInterceptor(publicApiInterceptor)
|
||||
.addPathPatterns("/public-api/**")
|
||||
.excludePathPatterns("/public-api/bot/chat")
|
||||
.excludePathPatterns(
|
||||
"/public-api/agent/chat",
|
||||
"/public-api/bot/chat"
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package tech.easyflow.publicapi.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.agent.runtime.AgentChatRequest;
|
||||
import tech.easyflow.agent.runtime.AgentRunService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link PublicAgentController} API Key 身份边界测试。
|
||||
*/
|
||||
public class PublicAgentControllerTest {
|
||||
|
||||
/**
|
||||
* 验证缺少 API Key 时返回明确的 HTTP 401 业务异常。
|
||||
*/
|
||||
@Test
|
||||
public void chatShouldRejectMissingApiKey() {
|
||||
PublicAgentController controller = new PublicAgentController(
|
||||
new RecordingAgentRunService(new ArrayList<>()),
|
||||
proxy(
|
||||
SysApiKeyService.class,
|
||||
(instance, method, args) -> {
|
||||
throw new AssertionError("缺少 API Key 时不应调用服务");
|
||||
}
|
||||
)
|
||||
);
|
||||
HttpServletRequest request = proxy(
|
||||
HttpServletRequest.class,
|
||||
(instance, method, args) -> {
|
||||
if ("getHeader".equals(method.getName())) {
|
||||
return " ";
|
||||
}
|
||||
throw new AssertionError("测试路径不应调用 HttpServletRequest." + method.getName());
|
||||
}
|
||||
);
|
||||
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> controller.chat(new AgentChatRequest(), request)
|
||||
);
|
||||
|
||||
Assert.assertEquals(401, exception.getHttpStatus());
|
||||
Assert.assertEquals("Apikey不能为空!", exception.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证接口先校验 API Key,并将其租户与部门传入 Agent 运行时。
|
||||
*/
|
||||
@Test
|
||||
public void chatShouldAuthorizeBeforeRunningWithApiKeyTenant() {
|
||||
List<String> calls = new ArrayList<>();
|
||||
SysApiKey apiKey = new SysApiKey();
|
||||
apiKey.setId(BigInteger.valueOf(101));
|
||||
apiKey.setTenantId(BigInteger.valueOf(201));
|
||||
apiKey.setDeptId(BigInteger.valueOf(301));
|
||||
SysApiKeyService apiKeyService = proxy(
|
||||
SysApiKeyService.class,
|
||||
(instance, method, args) -> {
|
||||
if ("checkApikeyPermission".equals(method.getName())) {
|
||||
calls.add("permission");
|
||||
Assert.assertEquals("test-key", args[0]);
|
||||
Assert.assertEquals("/public-api/agent/chat", args[1]);
|
||||
return null;
|
||||
}
|
||||
if ("getSysApiKey".equals(method.getName())) {
|
||||
calls.add("load");
|
||||
return apiKey;
|
||||
}
|
||||
throw new AssertionError("测试路径不应调用 SysApiKeyService." + method.getName());
|
||||
}
|
||||
);
|
||||
RecordingAgentRunService runService = new RecordingAgentRunService(calls);
|
||||
PublicAgentController controller = new PublicAgentController(runService, apiKeyService);
|
||||
HttpServletRequest request = proxy(
|
||||
HttpServletRequest.class,
|
||||
(instance, method, args) -> {
|
||||
if ("getHeader".equals(method.getName())) {
|
||||
return "test-key";
|
||||
}
|
||||
if ("getRequestURI".equals(method.getName())) {
|
||||
return "/public-api/agent/chat";
|
||||
}
|
||||
throw new AssertionError("测试路径不应调用 HttpServletRequest." + method.getName());
|
||||
}
|
||||
);
|
||||
|
||||
SseEmitter result = controller.chat(new AgentChatRequest(), request);
|
||||
|
||||
Assert.assertSame(runService.emitter, result);
|
||||
Assert.assertEquals(List.of("permission", "load", "chat"), calls);
|
||||
Assert.assertEquals(apiKey.getId(), runService.account.getId());
|
||||
Assert.assertEquals(apiKey.getTenantId(), runService.account.getTenantId());
|
||||
Assert.assertEquals(apiKey.getDeptId(), runService.account.getDeptId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建接口代理。
|
||||
*
|
||||
* @param type 接口类型
|
||||
* @param handler 调用处理器
|
||||
* @param <T> 接口类型
|
||||
* @return 代理实例
|
||||
*/
|
||||
private <T> T proxy(Class<T> type, java.lang.reflect.InvocationHandler handler) {
|
||||
return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录公共 Agent 调用身份的运行服务。
|
||||
*/
|
||||
private static class RecordingAgentRunService extends AgentRunService {
|
||||
|
||||
private final List<String> calls;
|
||||
private final SseEmitter emitter = new SseEmitter();
|
||||
private LoginAccount account;
|
||||
|
||||
/**
|
||||
* 创建运行服务桩。
|
||||
*
|
||||
* @param calls 调用顺序记录
|
||||
*/
|
||||
private RecordingAgentRunService(List<String> calls) {
|
||||
this.calls = calls;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 API Key 调用身份。
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param apiAccount API Key 调用身份
|
||||
* @return 测试用 SSE Emitter
|
||||
*/
|
||||
@Override
|
||||
public SseEmitter chatPublic(AgentChatRequest chatRequest, LoginAccount apiAccount) {
|
||||
calls.add("chat");
|
||||
account = apiAccount;
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user