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

@@ -6,8 +6,9 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.admin.controller.ai.support.BotResourceCreatorNameSupport;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.service.AiResourceApprovalStateService;
import tech.easyflow.ai.service.BotApprovalStateService;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.BotMessageService;
import tech.easyflow.ai.service.BotService;
@@ -38,7 +39,7 @@ public class BotControllerTest {
private BotDocumentCollectionService botDocumentCollectionService;
private BotMessageService botMessageService;
private CategoryPermissionService categoryPermissionService;
private AiResourceApprovalStateService aiResourceApprovalStateService;
private BotApprovalStateService botApprovalStateService;
private SysAccountService sysAccountService;
/**
@@ -52,7 +53,7 @@ public class BotControllerTest {
botDocumentCollectionService = mock(BotDocumentCollectionService.class);
botMessageService = mock(BotMessageService.class);
categoryPermissionService = mock(CategoryPermissionService.class);
aiResourceApprovalStateService = mock(AiResourceApprovalStateService.class);
botApprovalStateService = mock(BotApprovalStateService.class);
sysAccountService = mock(SysAccountService.class);
}
@@ -69,10 +70,12 @@ public class BotControllerTest {
botMessageService
);
AiResourceCreatorNameSupport creatorNameSupport = new AiResourceCreatorNameSupport();
BotResourceCreatorNameSupport botCreatorNameSupport =
new BotResourceCreatorNameSupport(creatorNameSupport);
setField(creatorNameSupport, "sysAccountService", sysAccountService);
setField(controller, "categoryPermissionService", categoryPermissionService);
setField(controller, "aiResourceApprovalStateService", aiResourceApprovalStateService);
setField(controller, "aiResourceCreatorNameSupport", creatorNameSupport);
setField(controller, "botApprovalStateService", botApprovalStateService);
setField(controller, "botResourceCreatorNameSupport", botCreatorNameSupport);
Bot bot = new Bot();
bot.setId(BigInteger.valueOf(101));
@@ -84,7 +87,7 @@ public class BotControllerTest {
when(botService.page(any(Page.class), any(QueryWrapper.class))).thenReturn(page);
when(sysAccountService.resolveDisplayNameMap(Collections.singleton(BigInteger.valueOf(7))))
.thenReturn(Map.of(BigInteger.valueOf(7), "管理员"));
doNothing().when(aiResourceApprovalStateService).fillBotApprovalState(page.getRecords());
doNothing().when(botApprovalStateService).fillApprovalState(page.getRecords());
Page<Bot> result = controller.invokeQueryPage(new Page<>(1, 10), QueryWrapper.create());

View File

@@ -0,0 +1,71 @@
package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import java.math.BigInteger;
import java.util.Locale;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link McpController} 删除锁测试。
*/
public class McpControllerTest {
/**
* 验证 MCP 删除先锁定资源行,再执行删除。
*/
@Test
public void removeShouldLockMcpBeforeRemoval() {
McpService mcpService = mock(McpService.class);
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
when(mcpService.getOne(any(QueryWrapper.class))).thenReturn(new Mcp());
McpController controller = new McpController(mcpService);
setField(controller, "agentResourceReferenceService", referenceService);
LoginAccount loginAccount = new LoginAccount();
loginAccount.setTenantId(BigInteger.ONE);
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
login.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
controller.remove(BigInteger.TEN);
}
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
verify(mcpService).getOne(queryCaptor.capture());
Assert.assertTrue(
queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT).contains("FOR UPDATE")
);
verify(referenceService).assertMcpUnused(BigInteger.TEN);
verify(mcpService).removeMcp(BigInteger.TEN);
}
/**
* 通过反射设置字段值。
*
* @param target 目标对象
* @param fieldName 字段名
* @param value 字段值
*/
private static void setField(Object target, String fieldName, Object value) {
try {
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
}
}
}

View File

@@ -0,0 +1,30 @@
package tech.easyflow.admin.controller.ai;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.service.PluginService;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link PluginController} 删除接口测试。
*/
public class PluginControllerTest {
/**
* 插件删除接口必须委托事务服务执行完整引用校验和删除。
*/
@Test
public void removeShouldDelegateToTransactionalService() {
PluginService pluginService = mock(PluginService.class);
when(pluginService.removePlugin("10")).thenReturn(true);
PluginController controller = new PluginController(pluginService);
boolean removed = controller.removePlugin("10").getData();
Assert.assertTrue(removed);
verify(pluginService).removePlugin("10");
}
}

View File

@@ -0,0 +1,101 @@
package tech.easyflow.admin.controller.ai;
import com.mybatisflex.core.query.QueryWrapper;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Plugin;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.service.AgentResourceReferenceService;
import tech.easyflow.ai.service.PluginItemService;
import tech.easyflow.ai.service.PluginService;
import tech.easyflow.ai.service.PluginVisibilityService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import java.math.BigInteger;
import java.util.List;
import java.util.Locale;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link PluginItemController} 删除锁测试。
*/
public class PluginItemControllerTest {
/**
* 验证插件工具删除按稳定顺序锁定资源行。
*/
@Test
public void removeCheckShouldLockPluginItemsInStableOrder() {
PluginItemService pluginItemService = mock(PluginItemService.class);
AgentResourceReferenceService referenceService = mock(AgentResourceReferenceService.class);
PluginService pluginService = mock(PluginService.class);
PluginVisibilityService visibilityService = mock(PluginVisibilityService.class);
PluginItem first = pluginItem(BigInteger.ONE, BigInteger.TEN);
PluginItem second = pluginItem(BigInteger.TWO, BigInteger.TEN);
Plugin plugin = new Plugin();
plugin.setId(BigInteger.TEN);
plugin.setTenantId(1L);
plugin.setCreatedBy(1L);
when(pluginItemService.list(any(QueryWrapper.class))).thenReturn(List.of(first, second));
when(pluginService.getById(BigInteger.TEN)).thenReturn(plugin);
PluginItemController controller = new PluginItemController(pluginItemService);
setField(controller, "pluginItemService", pluginItemService);
setField(controller, "agentResourceReferenceService", referenceService);
setField(controller, "pluginService", pluginService);
setField(controller, "pluginVisibilityService", visibilityService);
LoginAccount loginAccount = new LoginAccount();
loginAccount.setTenantId(BigInteger.ONE);
try (MockedStatic<SaTokenUtil> login = mockStatic(SaTokenUtil.class)) {
login.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
controller.onRemoveBefore(List.of(BigInteger.TWO, BigInteger.ONE));
}
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
verify(pluginItemService).list(queryCaptor.capture());
String sql = queryCaptor.getValue().toSQL().toUpperCase(Locale.ROOT);
Assert.assertTrue(sql.contains("ORDER BY"));
Assert.assertTrue(sql.contains("FOR UPDATE"));
verify(referenceService).assertPluginItemsUnused(List.of(BigInteger.TWO, BigInteger.ONE));
verify(visibilityService).assertPluginVisible(1L, BigInteger.TEN, "无权限删除该插件工具");
}
/**
* 创建插件工具。
*
* @param id 工具 ID
* @param pluginId 插件 ID
* @return 插件工具
*/
private static PluginItem pluginItem(BigInteger id, BigInteger pluginId) {
PluginItem pluginItem = new PluginItem();
pluginItem.setId(id);
pluginItem.setPluginId(pluginId);
return pluginItem;
}
/**
* 通过反射设置字段值。
*
* @param target 目标对象
* @param fieldName 字段名
* @param value 字段值
*/
private static void setField(Object target, String fieldName, Object value) {
try {
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("设置测试字段失败: " + fieldName, e);
}
}
}

View File

@@ -4,8 +4,8 @@ import com.mybatisflex.core.query.QueryWrapper;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.service.BotService;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.service.AgentService;
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
@@ -22,6 +22,7 @@ import tech.easyflow.chatlog.service.ChatDashboardQueryService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysAccountRoleService;
@@ -339,29 +340,34 @@ public class DashboardServiceImplTest {
public void shouldQueryUserRanksWithAssistantFilter() throws Exception {
DashboardServiceImpl service = new DashboardServiceImpl();
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
BotService botService = mock(BotService.class);
AgentService agentService = mock(AgentService.class);
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
SysAccountService sysAccountService = mock(SysAccountService.class);
SysAccountRoleService sysAccountRoleService = mock(SysAccountRoleService.class);
SysRoleService sysRoleService = mock(SysRoleService.class);
Bot bot = new Bot();
bot.setId(BigInteger.TEN);
bot.setStatus(1);
bot.setCreatedBy(BigInteger.ONE);
bot.setCategoryId(BigInteger.valueOf(8));
Agent agent = new Agent();
agent.setId(BigInteger.TEN);
agent.setStatus(1);
agent.setCreatedBy(BigInteger.ONE);
agent.setCategoryId(BigInteger.valueOf(8));
when(chatDashboardQueryService.available()).thenReturn(true);
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5)))
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 2L, 4L, 1L)));
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
when(agentService.getById(BigInteger.TEN)).thenReturn(agent);
when(categoryPermissionService.canAccessCategory(
any(LoginAccount.class),
eq(CategoryResourceType.AGENT.getCode()),
eq(BigInteger.ONE),
eq(BigInteger.valueOf(8))
))
.thenReturn(true);
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(2L, "demo-user", "演示用户")));
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
setField(service, "botService", botService);
setField(service, "agentService", agentService);
setField(service, "categoryPermissionService", categoryPermissionService);
setField(service, "sysAccountService", sysAccountService);
setField(service, "sysAccountRoleService", sysAccountRoleService);
@@ -382,26 +388,59 @@ public class DashboardServiceImplTest {
}
/**
* 验证未启用智能体会被拒绝
* 验证用智能体仍可用于筛选历史统计
*/
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
public void shouldRejectDisabledAssistantFilter() {
@Test
public void shouldAllowDisabledAgentFilterForHistoricalStatistics() {
DashboardServiceImpl service = new DashboardServiceImpl();
BotService botService = mock(BotService.class);
AgentService agentService = mock(AgentService.class);
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
Bot bot = new Bot();
bot.setId(BigInteger.TEN);
bot.setStatus(0);
Agent agent = new Agent();
agent.setId(BigInteger.TEN);
agent.setStatus(0);
agent.setCreatedBy(BigInteger.ONE);
agent.setCategoryId(BigInteger.valueOf(8));
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
when(agentService.getById(BigInteger.TEN)).thenReturn(agent);
when(chatDashboardQueryService.available()).thenReturn(true);
when(categoryPermissionService.canAccessCategory(
any(LoginAccount.class),
eq(CategoryResourceType.AGENT.getCode()),
eq(BigInteger.ONE),
eq(BigInteger.valueOf(8))
)).thenReturn(true);
setFieldSilently(service, "botService", botService);
setFieldSilently(service, "agentService", agentService);
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
setFieldSilently(service, "categoryPermissionService", mock(CategoryPermissionService.class));
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
DashboardUserRankQuery query = new DashboardUserRankQuery();
query.setRange("7d");
query.setAssistantId(BigInteger.TEN);
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(new LoginAccount(), query);
Assert.assertTrue(userRanks.isEmpty());
verify(chatDashboardQueryService).queryActiveUserRanks(
any(),
any(),
any(),
eq(BigInteger.TEN),
eq(5)
);
}
/**
* 验证不存在的智能体筛选会被拒绝。
*/
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "智能体不存在或不可见")
public void shouldRejectMissingAgentFilter() {
DashboardServiceImpl service = new DashboardServiceImpl();
AgentService agentService = mock(AgentService.class);
setFieldSilently(service, "agentService", agentService);
DashboardUserRankQuery query = new DashboardUserRankQuery();
query.setRange("7d");
query.setAssistantId(BigInteger.TEN);
@@ -411,25 +450,30 @@ public class DashboardServiceImplTest {
/**
* 验证当前作用域不可见的智能体会被拒绝。
*/
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "智能体不存在或不可见")
public void shouldRejectInvisibleAssistantFilter() {
DashboardServiceImpl service = new DashboardServiceImpl();
BotService botService = mock(BotService.class);
AgentService agentService = mock(AgentService.class);
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
Bot bot = new Bot();
bot.setId(BigInteger.TEN);
bot.setStatus(1);
bot.setCreatedBy(BigInteger.ONE);
bot.setCategoryId(BigInteger.valueOf(8));
Agent agent = new Agent();
agent.setId(BigInteger.TEN);
agent.setStatus(1);
agent.setCreatedBy(BigInteger.ONE);
agent.setCategoryId(BigInteger.valueOf(8));
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
when(agentService.getById(BigInteger.TEN)).thenReturn(agent);
when(chatDashboardQueryService.available()).thenReturn(true);
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
when(categoryPermissionService.canAccessCategory(
any(LoginAccount.class),
eq(CategoryResourceType.AGENT.getCode()),
eq(BigInteger.ONE),
eq(BigInteger.valueOf(8))
))
.thenReturn(false);
setFieldSilently(service, "botService", botService);
setFieldSilently(service, "agentService", agentService);
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));