fix: 完成系统向智能体数据链路切换
- 切换工作台、聊天历史、资源候选与公共调用到 Agent - 加固资源绑定、删除保护及发布运行并发控制 - 隔离旧 Bot 专属服务和组件并保留兼容入口
This commit is contained in:
@@ -35,22 +35,23 @@ import tech.easyflow.agent.runtime.document.AgentDocumentUploadView;
|
||||
import tech.easyflow.agent.runtime.media.AgentMediaService;
|
||||
import tech.easyflow.agent.runtime.media.AgentMediaUploadView;
|
||||
import com.easyagents.agent.runtime.media.AgentMediaResource;
|
||||
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
|
||||
import tech.easyflow.agent.service.AgentApprovalStateService;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.agent.vo.AgentResourceOptionsView;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.log.annotation.LogReporterDisabled;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
@@ -60,8 +61,6 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static tech.easyflow.agent.entity.table.AgentTableDef.AGENT;
|
||||
|
||||
/**
|
||||
* Agent 管理端控制器。
|
||||
*/
|
||||
@@ -78,10 +77,6 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
@Resource
|
||||
private AgentPublishAppService agentPublishAppService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private AgentApprovalStateService agentApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
@@ -91,6 +86,10 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
private AgentDocumentService agentDocumentService;
|
||||
@Resource
|
||||
private AgentComposerDraftService agentComposerDraftService;
|
||||
@Resource
|
||||
private AgentOptionQueryService agentOptionQueryService;
|
||||
@Resource
|
||||
private AgentVisibilityQueryHelper agentVisibilityQueryHelper;
|
||||
|
||||
/**
|
||||
* 创建 Agent 控制器。
|
||||
@@ -151,20 +150,67 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
public Result<List<Agent>> list(Agent entity, Boolean asTree, String sortKey, String sortType) {
|
||||
HttpServletRequest request = currentRequest();
|
||||
QueryWrapper queryWrapper = request == null ? QueryWrapper.create() : buildQueryWrapper(request);
|
||||
if (!applyCategoryPermission(queryWrapper)) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
List<Agent> agents = service.list(queryWrapper);
|
||||
if (isPublishedOnlyRequest()) {
|
||||
agents = agents.stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList();
|
||||
}
|
||||
agents.forEach(this::sanitizeListItem);
|
||||
agentApprovalStateService.fillAgentApprovalState(agents);
|
||||
aiResourceCreatorNameSupport.fillAgentCreatorNames(agents);
|
||||
return Result.ok(agents);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 安全选择项。
|
||||
*
|
||||
* @param publishedOnly 是否仅返回已发布 Agent
|
||||
* @return Agent 安全选择项
|
||||
*/
|
||||
@GetMapping("/options")
|
||||
@SaCheckPermission("/api/v1/agent/query")
|
||||
public Result<List<AgentOptionView>> options(
|
||||
@RequestParam(value = "publishedOnly", defaultValue = "false") boolean publishedOnly) {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(publishedOnly));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 设计器的安全资源选项。
|
||||
*
|
||||
* @return 设计器资源选项
|
||||
*/
|
||||
@GetMapping("/resourceOptions")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<AgentResourceOptionsView> resourceOptions() {
|
||||
return Result.ok(agentOptionQueryService.listDesignerResourceOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Agent 会话可使用的知识库安全选项。
|
||||
*
|
||||
* @return 知识库选项
|
||||
*/
|
||||
@GetMapping("/knowledgeOptions")
|
||||
@SaCheckPermission("/api/v1/agent/query")
|
||||
public Result<List<AgentResourceOptionsView.ResourceOption>> knowledgeOptions() {
|
||||
return Result.ok(agentOptionQueryService.listKnowledgeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 MCP 的安全工具列表。
|
||||
*
|
||||
* @param id MCP ID
|
||||
* @return MCP 工具列表
|
||||
*/
|
||||
@GetMapping("/mcpToolOptions")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<List<AgentResourceOptionsView.McpToolOption>> mcpToolOptions(
|
||||
@RequestParam BigInteger id) {
|
||||
return Result.ok(agentOptionQueryService.listMcpTools(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 Agent 纯文本聊天。
|
||||
*
|
||||
@@ -488,16 +534,13 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
for (Serializable id : ids) {
|
||||
Agent agent = service.getById(String.valueOf(id));
|
||||
if (agent != null) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限删除该 Agent");
|
||||
}
|
||||
}
|
||||
agentToolBindingService.remove(QueryWrapper.create().in("agent_id", ids));
|
||||
agentKnowledgeBindingService.remove(QueryWrapper.create().in("agent_id", ids));
|
||||
return super.onRemoveBefore(ids);
|
||||
public Result<?> remove(Serializable id) {
|
||||
throw new BusinessException("Agent 仅支持通过生命周期审批删除");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> removeBatch(Collection<Serializable> ids) {
|
||||
throw new BusinessException("Agent 仅支持通过生命周期审批删除");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,38 +552,54 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
*/
|
||||
@Override
|
||||
protected Page<Agent> queryPage(Page<Agent> page, QueryWrapper queryWrapper) {
|
||||
if (!applyCategoryPermission(queryWrapper)) {
|
||||
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), 0L);
|
||||
}
|
||||
agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
Page<Agent> result = super.queryPage(page, queryWrapper);
|
||||
Page<Agent> result = service.page(page, queryWrapper);
|
||||
if (isPublishedOnlyRequest()) {
|
||||
result.setRecords(result.getRecords().stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList());
|
||||
}
|
||||
result.getRecords().forEach(this::sanitizeListItem);
|
||||
agentApprovalStateService.fillAgentApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillAgentCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
|
||||
if (!access.isRestricted()) {
|
||||
return true;
|
||||
/**
|
||||
* 清理列表无需返回的配置和发布快照,避免敏感运行配置进入浏览器。
|
||||
*
|
||||
* @param agent Agent 列表项
|
||||
*/
|
||||
private void sanitizeListItem(Agent agent) {
|
||||
if (agent == null) {
|
||||
return;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Agent::getCreatedBy, access.getAccountId());
|
||||
return true;
|
||||
}
|
||||
queryWrapper.and(AGENT.CREATED_BY.eq(access.getAccountId()).or(AGENT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
return true;
|
||||
agent.setModelConfigJson(Collections.emptyMap());
|
||||
agent.setGenerationConfigJson(Collections.emptyMap());
|
||||
agent.setPromptConfigJson(Collections.emptyMap());
|
||||
agent.setMemoryConfigJson(Collections.emptyMap());
|
||||
agent.setExecutionConfigJson(Collections.emptyMap());
|
||||
agent.setInteractionConfigJson(Collections.emptyMap());
|
||||
agent.setPublishedSnapshotJson(Collections.emptyMap());
|
||||
agent.setToolBindings(null);
|
||||
agent.setKnowledgeBindings(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为仅发布查询追加发布状态条件。
|
||||
*
|
||||
* @param queryWrapper Agent 查询条件
|
||||
*/
|
||||
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||
if (isPublishedOnlyRequest()) {
|
||||
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前请求是否只查询已发布 Agent。
|
||||
*
|
||||
* @return 是否仅查询已发布 Agent
|
||||
*/
|
||||
private boolean isPublishedOnlyRequest() {
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request == null) {
|
||||
@@ -562,6 +621,14 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
return attributes.getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将审批执行结果转换为统一响应。
|
||||
*
|
||||
* @param actionResult 审批动作结果
|
||||
* @param approvalMessage 进入审批时的提示
|
||||
* @param directMessage 直接执行时的提示
|
||||
* @return 审批实例响应
|
||||
*/
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -130,7 +131,17 @@ public class AgentSessionController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录信息失效时抛出
|
||||
*/
|
||||
private LoginAccount currentAccount() {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.admin.controller.ai.support.BotResourceCreatorNameSupport;
|
||||
import tech.easyflow.admin.service.ai.ChatWorkspaceService;
|
||||
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
||||
@@ -82,9 +82,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
@Resource
|
||||
private ChatRoundOperateService chatRoundOperateService;
|
||||
@Resource
|
||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||
private BotApprovalStateService botApprovalStateService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
private BotResourceCreatorNameSupport botResourceCreatorNameSupport;
|
||||
@Resource
|
||||
private ChatWorkspaceService chatWorkspaceService;
|
||||
|
||||
@@ -240,7 +240,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
bot = botService.toPublishedView(rawBot);
|
||||
}
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(bot);
|
||||
botApprovalStateService.fillApprovalState(bot);
|
||||
}
|
||||
return Result.ok(bot);
|
||||
}
|
||||
@@ -275,7 +275,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
|
||||
if (data.getModelId() == null) {
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||
botApprovalStateService.fillApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
@@ -286,7 +286,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
if (llm == null) {
|
||||
data.setModelId(null);
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||
botApprovalStateService.fillApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
@@ -302,7 +302,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
}
|
||||
|
||||
if (StpUtil.isLogin()) {
|
||||
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||
botApprovalStateService.fillApprovalState(data);
|
||||
}
|
||||
return Result.ok(data);
|
||||
}
|
||||
@@ -369,7 +369,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
if (isPublishedOnlyRequest()) {
|
||||
bots = bots.stream().map(botService::toPublishedView).toList();
|
||||
}
|
||||
aiResourceApprovalStateService.fillBotApprovalState(bots);
|
||||
botApprovalStateService.fillApprovalState(bots);
|
||||
return Result.ok(bots);
|
||||
}
|
||||
|
||||
@@ -381,8 +381,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
if (isPublishedOnlyRequest()) {
|
||||
result.setRecords(result.getRecords().stream().map(botService::toPublishedView).toList());
|
||||
}
|
||||
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
|
||||
botApprovalStateService.fillApprovalState(result.getRecords());
|
||||
botResourceCreatorNameSupport.fillCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 旧 Bot 插件工具绑定查询控制器。
|
||||
*
|
||||
* <p>保留历史接口地址,同时将 Bot 表依赖限制在 Bot 专属代码中。</p>
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pluginItem")
|
||||
@UsePermission(moduleName = "/api/v1/plugin")
|
||||
public class BotPluginItemController {
|
||||
|
||||
private final BotPluginService botPluginService;
|
||||
|
||||
/**
|
||||
* 创建 Bot 插件工具绑定查询控制器。
|
||||
*
|
||||
* @param botPluginService Bot 插件绑定服务
|
||||
*/
|
||||
public BotPluginItemController(BotPluginService botPluginService) {
|
||||
this.botPluginService = botPluginService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询插件工具,并标记指定 Bot 已绑定的工具。
|
||||
*
|
||||
* @param pluginId 插件 ID
|
||||
* @param botId Bot ID
|
||||
* @return 插件工具列表
|
||||
*/
|
||||
@PostMapping("/toolsList")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> searchPluginTools(
|
||||
@JsonBody(value = "pluginId", required = true) BigInteger pluginId,
|
||||
@JsonBody(value = "botId", required = false) BigInteger botId) {
|
||||
return Result.ok(botPluginService.searchPluginTools(pluginId, botId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定 Bot 已绑定的插件工具。
|
||||
*
|
||||
* @param botId Bot ID
|
||||
* @return 已绑定插件工具列表
|
||||
*/
|
||||
@PostMapping("/tool/list")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> getPluginTools(
|
||||
@JsonBody(value = "botId", required = true) BigInteger botId) {
|
||||
return Result.ok(botPluginService.getPluginTools(botId));
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,11 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
@@ -27,7 +28,6 @@ import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
@@ -68,7 +68,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
private final ModelService llmService;
|
||||
|
||||
@Resource
|
||||
private BotDocumentCollectionService botDocumentCollectionService;
|
||||
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
@@ -169,11 +169,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
||||
queryWrapper.in(AgentKnowledgeBinding::getKnowledgeId, ids);
|
||||
|
||||
boolean exists = botDocumentCollectionService.exists(queryWrapper);
|
||||
boolean exists = agentKnowledgeBindingService.exists(queryWrapper);
|
||||
if (exists){
|
||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||
throw new BusinessException("此知识库仍被智能体使用,请先取消绑定后再删除");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,16 +9,19 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.BotMcp;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.service.BotMcpService;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -34,7 +37,7 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
||||
}
|
||||
|
||||
@Resource
|
||||
private BotMcpService botMcpService;
|
||||
private AgentResourceReferenceService agentResourceReferenceService;
|
||||
@Override
|
||||
public Result<?> save(Mcp entity) {
|
||||
return service.saveMcp(entity);
|
||||
@@ -45,11 +48,29 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
||||
return service.updateMcp(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除未被 Agent 绑定的 MCP。
|
||||
*
|
||||
* @param id MCP ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> remove(Serializable id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
// 锁定 MCP 资源行,与 Agent 绑定校验串行,避免检查后并发写入绑定。
|
||||
Mcp mcp = service.getOne(QueryWrapper.create()
|
||||
.eq(Mcp::getId, id)
|
||||
.eq(Mcp::getTenantId, account.getTenantId())
|
||||
.forUpdate());
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("MCP 不存在或无权删除");
|
||||
}
|
||||
agentResourceReferenceService.assertMcpUnused(new BigInteger(String.valueOf(id)));
|
||||
service.removeMcp(id);
|
||||
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getMcpId, id));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import tech.easyflow.ai.dto.ModelInvokeConfigDtos;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.table.ModelTableDef;
|
||||
import tech.easyflow.ai.mapper.ModelMapper;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityResolution;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -18,6 +19,7 @@ import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -47,6 +49,8 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
|
||||
@Resource
|
||||
ModelMapper modelMapper;
|
||||
@Resource
|
||||
AgentResourceReferenceService agentResourceReferenceService;
|
||||
|
||||
@GetMapping("list")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
@@ -99,7 +103,22 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
|
||||
@PostMapping("/removeByEntity")
|
||||
@SaCheckPermission("/api/v1/model/remove")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> removeByEntity(@RequestBody Model entity) {
|
||||
LoginAccount account = requireAccount();
|
||||
List<Model> models = service.list(QueryWrapper.create()
|
||||
.select(Model::getId)
|
||||
.eq(Model::getProviderId, entity.getProviderId())
|
||||
.eq(Model::getGroupName, entity.getGroupName())
|
||||
.eq(Model::getTenantId, account.getTenantId())
|
||||
.orderBy(Model::getId, true)
|
||||
.forUpdate());
|
||||
if (models.isEmpty()) {
|
||||
throw new BusinessException("模型不存在或无权删除");
|
||||
}
|
||||
agentResourceReferenceService.assertModelsUnused(
|
||||
models.stream().map(Model::getId).toList());
|
||||
entity.setTenantId(account.getTenantId());
|
||||
modelService.removeByEntity(entity);
|
||||
return Result.ok();
|
||||
}
|
||||
@@ -172,11 +191,39 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
}
|
||||
|
||||
@PostMapping("removeLlmByIds")
|
||||
@Transactional
|
||||
@SaCheckPermission("/api/v1/model/remove")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<?> removeLlm(@JsonBody(value = "id", required = true) Serializable id) {
|
||||
LoginAccount account = requireAccount();
|
||||
List<Serializable> ids = Collections.singletonList(id);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create().in(Model::getId, ids);
|
||||
service.remove(queryWrapper);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.in(Model::getId, ids)
|
||||
.eq(Model::getTenantId, account.getTenantId())
|
||||
.orderBy(Model::getId, true)
|
||||
.forUpdate();
|
||||
List<Model> models = service.list(queryWrapper);
|
||||
if (models.isEmpty()) {
|
||||
throw new BusinessException("模型不存在或无权删除");
|
||||
}
|
||||
agentResourceReferenceService.assertModelsUnused(
|
||||
models.stream().map(Model::getId).toList());
|
||||
service.remove(QueryWrapper.create()
|
||||
.in(Model::getId, ids)
|
||||
.eq(Model::getTenantId, account.getTenantId()));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带租户信息的当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录状态无效时抛出
|
||||
*/
|
||||
private LoginAccount requireAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
@@ -47,8 +48,14 @@ import static tech.easyflow.ai.entity.table.PluginTableDef.PLUGIN;
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/plugin")
|
||||
public class PluginController extends BaseCurdController<PluginService, Plugin> {
|
||||
/**
|
||||
* 创建插件控制器。
|
||||
*
|
||||
* @param service 插件服务
|
||||
*/
|
||||
public PluginController(PluginService service) {
|
||||
super(service);
|
||||
this.pluginService = service;
|
||||
}
|
||||
|
||||
@Resource
|
||||
@@ -89,10 +96,15 @@ public class PluginController extends BaseCurdController<PluginService, Plugin>
|
||||
return Result.ok(pluginService.updatePlugin(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件。
|
||||
*
|
||||
* @param id 插件 ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@PostMapping("/plugin/remove")
|
||||
@SaCheckPermission("/api/v1/plugin/remove")
|
||||
public Result<Boolean> removePlugin(@JsonBody(value = "id", required = true) String id){
|
||||
|
||||
return Result.ok(pluginService.removePlugin(id));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,19 +19,20 @@ import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.BotPlugin;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PluginType;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.BotPluginService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.AgentResourceReferenceService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
@@ -42,8 +43,10 @@ import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -55,6 +58,11 @@ import java.util.Map;
|
||||
@RequestMapping("/api/v1/pluginItem")
|
||||
@UsePermission(moduleName = "/api/v1/plugin")
|
||||
public class PluginItemController extends BaseCurdController<PluginItemService, PluginItem> {
|
||||
/**
|
||||
* 创建插件工具控制器。
|
||||
*
|
||||
* @param service 插件工具服务
|
||||
*/
|
||||
public PluginItemController(PluginItemService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -63,10 +71,12 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
private PluginItemService pluginItemService;
|
||||
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
private AgentResourceReferenceService agentResourceReferenceService;
|
||||
@Resource
|
||||
private PluginService pluginService;
|
||||
@Resource
|
||||
private PluginVisibilityService pluginVisibilityService;
|
||||
@Resource
|
||||
private WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@@ -91,25 +101,12 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
return pluginItemService.searchPlugin(aiPluginToolId);
|
||||
}
|
||||
|
||||
@PostMapping("/toolsList")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> searchPluginToolByPluginId(@JsonBody(value = "pluginId", required = true) BigInteger pluginId,
|
||||
@JsonBody(value = "botId", required = false) BigInteger botId){
|
||||
return Result.ok(pluginItemService.searchPluginToolByPluginId(pluginId, botId));
|
||||
}
|
||||
|
||||
@PostMapping("/tool/update")
|
||||
@SaCheckPermission("/api/v1/plugin/save")
|
||||
public Result<Boolean> updatePlugin(@JsonBody PluginItem pluginItem){
|
||||
return Result.ok(pluginItemService.updatePlugin(pluginItem));
|
||||
}
|
||||
|
||||
@PostMapping("/tool/list")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<List<PluginItem>> getPluginToolList(@JsonBody(value = "botId", required = true) BigInteger botId){
|
||||
return Result.ok(pluginItemService.getPluginToolList(botId));
|
||||
}
|
||||
|
||||
@GetMapping("/getTinyFlowData")
|
||||
@SaCheckPermission("/api/v1/plugin/query")
|
||||
public Result<?> getTinyFlowData(BigInteger id) {
|
||||
@@ -275,26 +272,64 @@ public class PluginItemController extends BaseCurdController<PluginItemService,
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除插件工具前锁定资源并校验 Agent 绑定。
|
||||
*
|
||||
* @param ids 插件工具 ID 集合
|
||||
* @return 校验失败结果;允许删除时返回 {@code null}
|
||||
*/
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(BotPlugin::getPluginItemId, ids);
|
||||
|
||||
boolean exists = botPluginService.exists(queryWrapper);
|
||||
if (exists){
|
||||
return Result.fail(1, "此工具还关联着bot,请先取消关联!");
|
||||
}
|
||||
if (ids.size() == 1) {
|
||||
PluginItem pluginItem = pluginItemService.getById(ids.iterator().next());
|
||||
if (pluginItem != null) {
|
||||
Plugin plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin != null && PluginType.isWorkflow(plugin.getType())) {
|
||||
return Result.fail(1, "工作流插件工具由系统自动维护,不支持删除");
|
||||
Set<BigInteger> uniquePluginItemIds = new LinkedHashSet<>();
|
||||
try {
|
||||
for (Serializable id : ids) {
|
||||
if (id == null) {
|
||||
throw new NumberFormatException("null");
|
||||
}
|
||||
uniquePluginItemIds.add(new BigInteger(String.valueOf(id)));
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException("插件工具 ID 不合法");
|
||||
}
|
||||
|
||||
// BaseCurdController#remove 已开启事务;锁定工具行后校验权限和 Agent 引用。
|
||||
List<PluginItem> lockedPluginItems = pluginItemService.list(QueryWrapper.create()
|
||||
.in(PluginItem::getId, uniquePluginItemIds)
|
||||
.orderBy(PluginItem::getId, true)
|
||||
.forUpdate());
|
||||
if (lockedPluginItems == null || lockedPluginItems.size() != uniquePluginItemIds.size()) {
|
||||
throw new BusinessException("插件工具不存在或已被删除");
|
||||
}
|
||||
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || loginAccount.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录信息无效");
|
||||
}
|
||||
Map<BigInteger, Plugin> plugins = new HashMap<>();
|
||||
for (PluginItem pluginItem : lockedPluginItems) {
|
||||
if (pluginItem.getPluginId() == null) {
|
||||
throw new BusinessException("插件工具关联的插件不存在");
|
||||
}
|
||||
Plugin plugin = plugins.get(pluginItem.getPluginId());
|
||||
if (plugin == null) {
|
||||
plugin = pluginService.getById(pluginItem.getPluginId());
|
||||
if (plugin == null || plugin.getTenantId() == null
|
||||
|| !loginAccount.getTenantId().toString().equals(plugin.getTenantId().toString())) {
|
||||
throw new BusinessException("无权限删除该插件工具");
|
||||
}
|
||||
pluginVisibilityService.assertPluginVisible(
|
||||
plugin.getCreatedBy(),
|
||||
plugin.getId(),
|
||||
"无权限删除该插件工具"
|
||||
);
|
||||
plugins.put(pluginItem.getPluginId(), plugin);
|
||||
}
|
||||
if (PluginType.isWorkflow(plugin.getType())) {
|
||||
return Result.fail(1, "工作流插件工具由系统自动维护,不支持删除");
|
||||
}
|
||||
}
|
||||
|
||||
agentResourceReferenceService.assertPluginItemsUnused(List.copyOf(uniquePluginItemIds));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.ai.permission.WorkflowShareResourceAccessGrantProvider;
|
||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
@@ -31,7 +34,6 @@ import tech.easyflow.ai.publish.WorkflowPublishAppService;
|
||||
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
@@ -74,7 +76,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@Resource
|
||||
private SysApiKeyService apiKeyService;
|
||||
@Resource
|
||||
private BotWorkflowService botWorkflowService;
|
||||
private AgentToolBindingService agentToolBindingService;
|
||||
@Resource
|
||||
private ChainExecutor chainExecutor;
|
||||
@Resource
|
||||
@@ -558,11 +560,12 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
Workflow workflow = requireWorkflow(String.valueOf(id));
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
||||
}
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in("workflow_id", ids);
|
||||
boolean exists = botWorkflowService.exists(queryWrapper);
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(AgentToolBinding::getToolType, AgentToolType.WORKFLOW.name())
|
||||
.in(AgentToolBinding::getTargetId, ids);
|
||||
boolean exists = agentToolBindingService.exists(queryWrapper);
|
||||
if (exists) {
|
||||
return Result.fail(1, "此工作流还关联有bot,请先取消关联后再删除!");
|
||||
return Result.fail(1, "此工作流仍被智能体使用,请先取消绑定后再删除");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package tech.easyflow.admin.controller.ai.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
@@ -41,15 +40,6 @@ public class AiResourceCreatorNameSupport {
|
||||
fillCreatorNames(workflows, Workflow::getCreatedBy, Workflow::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充聊天助手创建人名称。
|
||||
*
|
||||
* @param bots 聊天助手集合
|
||||
*/
|
||||
public void fillBotCreatorNames(Collection<Bot> bots) {
|
||||
fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充知识库创建人名称。
|
||||
*
|
||||
@@ -94,7 +84,7 @@ public class AiResourceCreatorNameSupport {
|
||||
* @param createdByNameSetter 创建人名称回填函数
|
||||
* @param <T> 资源类型
|
||||
*/
|
||||
private <T> void fillCreatorNames(
|
||||
<T> void fillCreatorNames(
|
||||
Collection<T> resources,
|
||||
Function<T, Number> createdByGetter,
|
||||
BiConsumer<T, String> createdByNameSetter
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.admin.controller.ai.support;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 为旧 Bot 资源批量补充创建人展示名称。
|
||||
*/
|
||||
@Component
|
||||
public class BotResourceCreatorNameSupport {
|
||||
|
||||
private final AiResourceCreatorNameSupport creatorNameSupport;
|
||||
|
||||
/**
|
||||
* 创建 Bot 创建人名称填充组件。
|
||||
*
|
||||
* @param creatorNameSupport 通用 AI 资源创建人名称组件
|
||||
*/
|
||||
public BotResourceCreatorNameSupport(AiResourceCreatorNameSupport creatorNameSupport) {
|
||||
this.creatorNameSupport = creatorNameSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充 Bot 创建人名称。
|
||||
*
|
||||
* @param bots Bot 集合
|
||||
*/
|
||||
public void fillCreatorNames(Collection<Bot> bots) {
|
||||
creatorNameSupport.fillCreatorNames(bots, Bot::getCreatedBy, Bot::setCreatedByName);
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ public class DashboardDistributionItemVo {
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
private Long botTotal;
|
||||
|
||||
private Long workflowTotal;
|
||||
|
||||
private Long knowledgeBaseTotal;
|
||||
@@ -73,14 +71,6 @@ public class DashboardDistributionItemVo {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
|
||||
public Long getBotTotal() {
|
||||
return botTotal;
|
||||
}
|
||||
|
||||
public void setBotTotal(Long botTotal) {
|
||||
this.botTotal = botTotal;
|
||||
}
|
||||
|
||||
public Long getWorkflowTotal() {
|
||||
return workflowTotal;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ public class DashboardSummaryVo {
|
||||
|
||||
private Long activeUserTotal;
|
||||
|
||||
private Long botTotal;
|
||||
/** 智能体总数。 */
|
||||
private Long agentTotal;
|
||||
|
||||
private Long workflowTotal;
|
||||
|
||||
@@ -39,12 +40,22 @@ public class DashboardSummaryVo {
|
||||
this.activeUserTotal = activeUserTotal;
|
||||
}
|
||||
|
||||
public Long getBotTotal() {
|
||||
return botTotal;
|
||||
/**
|
||||
* 获取智能体总数。
|
||||
*
|
||||
* @return 智能体总数
|
||||
*/
|
||||
public Long getAgentTotal() {
|
||||
return agentTotal;
|
||||
}
|
||||
|
||||
public void setBotTotal(Long botTotal) {
|
||||
this.botTotal = botTotal;
|
||||
/**
|
||||
* 设置智能体总数。
|
||||
*
|
||||
* @param agentTotal 智能体总数
|
||||
*/
|
||||
public void setAgentTotal(Long agentTotal) {
|
||||
this.agentTotal = agentTotal;
|
||||
}
|
||||
|
||||
public Long getWorkflowTotal() {
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
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.DashboardChatStatusVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendPointVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||
@@ -33,6 +33,7 @@ import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysAccountRole;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysAccountRoleService;
|
||||
@@ -81,7 +82,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
private ChatDashboardQueryService chatDashboardQueryService;
|
||||
|
||||
@Resource
|
||||
private BotService botService;
|
||||
private AgentService agentService;
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@@ -156,7 +157,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
summary.setUserTotal(countScopedTable("tb_sys_account", "a", true, context));
|
||||
summary.setActiveUserTotal(countActiveUsers(context));
|
||||
summary.setBotTotal(countScopedTable("tb_bot", "b", false, context));
|
||||
summary.setAgentTotal(countScopedTable("tb_agent", "a", false, context));
|
||||
summary.setWorkflowTotal(countScopedTable("tb_workflow", "w", false, context));
|
||||
summary.setKnowledgeBaseTotal(countScopedTable("tb_document_collection", "d", false, context));
|
||||
summary.setChatMessageTotal(0L);
|
||||
@@ -863,18 +864,18 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
if (assistantId == null) {
|
||||
return null;
|
||||
}
|
||||
Bot bot = botService.getById(assistantId);
|
||||
if (bot == null || !Integer.valueOf(1).equals(bot.getStatus())) {
|
||||
throw new BusinessException("聊天助手不存在或未启用");
|
||||
Agent agent = agentService.getById(assistantId);
|
||||
if (agent == null) {
|
||||
throw new BusinessException("智能体不存在或不可见");
|
||||
}
|
||||
boolean visible = categoryPermissionService.canAccessCategory(
|
||||
loginAccount,
|
||||
"BOT",
|
||||
bot.getCreatedBy(),
|
||||
bot.getCategoryId()
|
||||
CategoryResourceType.AGENT.getCode(),
|
||||
agent.getCreatedBy(),
|
||||
agent.getCategoryId()
|
||||
);
|
||||
if (!visible) {
|
||||
throw new BusinessException("聊天助手不存在或未启用");
|
||||
throw new BusinessException("智能体不存在或不可见");
|
||||
}
|
||||
return assistantId;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@
|
||||
<artifactId>easyflow-api-usercenter</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-auth</artifactId>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package tech.easyflow.usercenter.controller.agent;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.usercenter.model.agent.UcAgentListItemVo;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户中心 Agent 查询接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/userCenter/agent")
|
||||
@UsePermission(moduleName = "/api/v1/agent")
|
||||
public class UcAgentController {
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AgentVisibilityQueryHelper agentVisibilityQueryHelper;
|
||||
|
||||
/**
|
||||
* 创建用户中心 Agent 控制器。
|
||||
*
|
||||
* @param agentService Agent 服务
|
||||
* @param agentVisibilityQueryHelper Agent 可见性查询助手
|
||||
*/
|
||||
public UcAgentController(AgentService agentService,
|
||||
AgentVisibilityQueryHelper agentVisibilityQueryHelper) {
|
||||
this.agentService = agentService;
|
||||
this.agentVisibilityQueryHelper = agentVisibilityQueryHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户可见且已发布的 Agent。
|
||||
*
|
||||
* @return Agent 列表项
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Result<List<UcAgentListItemVo>> list() {
|
||||
requireCurrentAccount();
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.select(
|
||||
Agent::getId,
|
||||
Agent::getPublishedSnapshotJson
|
||||
)
|
||||
.eq(Agent::getStatus, 1)
|
||||
.eq(Agent::getPublishStatus, PublishStatus.PUBLISHED.getCode());
|
||||
agentVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||
queryWrapper.orderBy(Agent::getModified, false);
|
||||
List<Agent> agents = agentService.list(queryWrapper);
|
||||
if (agents == null || agents.isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
return Result.ok(agents.stream()
|
||||
.map(agent -> toListItem(agent, agentService.fromSnapshot(agent.getPublishedSnapshotJson())))
|
||||
.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取并校验当前登录账号。
|
||||
*
|
||||
* @return 当前登录账号
|
||||
* @throws BusinessException 登录信息失效时抛出
|
||||
*/
|
||||
protected LoginAccount requireCurrentAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将已发布快照转换为最小用户端列表项。
|
||||
*
|
||||
* @param liveAgent 当前 Agent 记录
|
||||
* @param publishedAgent 已发布 Agent 快照
|
||||
* @return 用户端列表项
|
||||
*/
|
||||
private UcAgentListItemVo toListItem(Agent liveAgent, Agent publishedAgent) {
|
||||
return new UcAgentListItemVo(
|
||||
liveAgent.getId(),
|
||||
publishedAgent.getName(),
|
||||
publishedAgent.getDescription(),
|
||||
publishedAgent.getAvatar()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package tech.easyflow.usercenter.model.agent;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 用户中心已发布 Agent 列表项。
|
||||
*/
|
||||
public class UcAgentListItemVo {
|
||||
|
||||
private final BigInteger id;
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final String avatar;
|
||||
|
||||
/**
|
||||
* 创建 Agent 列表项。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @param name Agent 名称
|
||||
* @param description Agent 描述
|
||||
* @param avatar Agent 头像
|
||||
*/
|
||||
public UcAgentListItemVo(BigInteger id, String name, String description, String avatar) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent ID。
|
||||
*
|
||||
* @return Agent ID
|
||||
*/
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 名称。
|
||||
*
|
||||
* @return Agent 名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 描述。
|
||||
*
|
||||
* @return Agent 描述
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 头像。
|
||||
*
|
||||
* @return Agent 头像
|
||||
*/
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package tech.easyflow.usercenter.controller.agent;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.security.AgentVisibilityQueryHelper;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.usercenter.model.agent.UcAgentListItemVo;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link UcAgentController} 用户端 Agent 可见性测试。
|
||||
*/
|
||||
public class UcAgentControllerTest {
|
||||
|
||||
/**
|
||||
* 验证列表把可见性限制下推到查询,并仅返回发布快照的展示字段。
|
||||
*/
|
||||
@Test
|
||||
public void listShouldFilterVisibilityAndReturnPublishedSummary() {
|
||||
Agent visibleAgent = liveAgent(BigInteger.ONE, "草稿名称一", "发布名称一");
|
||||
AgentService agentService = proxy(
|
||||
AgentService.class,
|
||||
(instance, method, args) -> {
|
||||
if ("list".equals(method.getName())) {
|
||||
return List.of(visibleAgent);
|
||||
}
|
||||
if ("fromSnapshot".equals(method.getName())) {
|
||||
Map<?, ?> snapshot = (Map<?, ?>) args[0];
|
||||
Agent published = new Agent();
|
||||
published.setName(String.valueOf(snapshot.get("name")));
|
||||
published.setDescription(String.valueOf(snapshot.get("description")));
|
||||
published.setAvatar(String.valueOf(snapshot.get("avatar")));
|
||||
return published;
|
||||
}
|
||||
throw new AssertionError("测试路径不应调用 AgentService." + method.getName());
|
||||
}
|
||||
);
|
||||
RecordingVisibilityQueryHelper visibilityQueryHelper = new RecordingVisibilityQueryHelper();
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setTenantId(BigInteger.ONE);
|
||||
UcAgentController controller = new TestUcAgentController(
|
||||
agentService,
|
||||
visibilityQueryHelper,
|
||||
account
|
||||
);
|
||||
|
||||
List<UcAgentListItemVo> result = controller.list().getData();
|
||||
|
||||
Assert.assertEquals(1, result.size());
|
||||
Assert.assertEquals(visibleAgent.getId(), result.get(0).getId());
|
||||
Assert.assertEquals("发布名称一", result.get(0).getName());
|
||||
Assert.assertEquals("发布描述一", result.get(0).getDescription());
|
||||
Assert.assertEquals("avatar-一", result.get(0).getAvatar());
|
||||
Assert.assertTrue(visibilityQueryHelper.applied);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带草稿字段和发布快照的 Agent。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @param draftName 草稿名称
|
||||
* @param publishedName 发布名称
|
||||
* @return Agent
|
||||
*/
|
||||
private Agent liveAgent(BigInteger id, String draftName, String publishedName) {
|
||||
Agent agent = new Agent();
|
||||
agent.setId(id);
|
||||
agent.setName(draftName);
|
||||
String suffix = BigInteger.ONE.equals(id) ? "一" : "二";
|
||||
agent.setPublishedSnapshotJson(Map.of(
|
||||
"name", publishedName,
|
||||
"description", "发布描述" + suffix,
|
||||
"avatar", "avatar-" + suffix
|
||||
));
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建接口代理。
|
||||
*
|
||||
* @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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录可见性条件是否已下推到查询。
|
||||
*/
|
||||
private static final class RecordingVisibilityQueryHelper extends AgentVisibilityQueryHelper {
|
||||
|
||||
private boolean applied;
|
||||
|
||||
/**
|
||||
* 创建记录型可见性查询助手。
|
||||
*/
|
||||
private RecordingVisibilityQueryHelper() {
|
||||
super(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper) {
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用固定登录账号的用户中心 Agent 控制器。
|
||||
*/
|
||||
private static final class TestUcAgentController extends UcAgentController {
|
||||
|
||||
private final LoginAccount account;
|
||||
|
||||
/**
|
||||
* 创建测试控制器。
|
||||
*
|
||||
* @param agentService Agent 服务
|
||||
* @param visibilityQueryHelper 可见性查询助手
|
||||
* @param account 当前账号
|
||||
*/
|
||||
private TestUcAgentController(AgentService agentService,
|
||||
AgentVisibilityQueryHelper visibilityQueryHelper,
|
||||
LoginAccount account) {
|
||||
super(agentService, visibilityQueryHelper);
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected LoginAccount requireCurrentAccount() {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user