fix: 完成系统向智能体数据链路切换

- 切换工作台、聊天历史、资源候选与公共调用到 Agent

- 加固资源绑定、删除保护及发布运行并发控制

- 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
2026-07-31 14:24:15 +08:00
parent f0aba1eddd
commit f872eac1f9
114 changed files with 5997 additions and 874 deletions

View File

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

View File

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

View File

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