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

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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));

View File

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

View File

@@ -0,0 +1,77 @@
package tech.easyflow.publicapi.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.agent.runtime.AgentChatRequest;
import tech.easyflow.agent.runtime.AgentRunService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.service.SysApiKeyService;
/**
* Agent 公共调用接口。
*/
@RestController
@RequestMapping("/public-api/agent")
public class PublicAgentController {
private final AgentRunService agentRunService;
private final SysApiKeyService sysApiKeyService;
/**
* 创建 Agent 公共接口控制器。
*
* @param agentRunService Agent 运行服务
* @param sysApiKeyService API Key 服务
*/
public PublicAgentController(AgentRunService agentRunService,
SysApiKeyService sysApiKeyService) {
this.agentRunService = agentRunService;
this.sysApiKeyService = sysApiKeyService;
}
/**
* 通过 API Key 调用已发布 Agent。
*
* @param chatRequest Agent 聊天请求
* @param request HTTP 请求
* @return SSE Emitter
*/
@PostMapping("/chat")
public SseEmitter chat(@RequestBody AgentChatRequest chatRequest,
HttpServletRequest request) {
String apiKey = request.getHeader(SysApiKey.KEY_Apikey);
if (!StringUtils.hasText(apiKey)) {
throw new BusinessException(401, 401, "Apikey不能为空!");
}
sysApiKeyService.checkApikeyPermission(apiKey, request.getRequestURI());
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
return agentRunService.chatPublic(chatRequest, buildApiAccount(sysApiKey));
}
/**
* 将 API Key 转换为独立的聊天调用身份。
*
* @param sysApiKey API Key 记录
* @return 调用身份
*/
private LoginAccount buildApiAccount(SysApiKey sysApiKey) {
LoginAccount account = new LoginAccount();
account.setId(sysApiKey.getId());
account.setTenantId(sysApiKey.getTenantId() == null
? java.math.BigInteger.ZERO
: sysApiKey.getTenantId());
account.setDeptId(sysApiKey.getDeptId() == null
? java.math.BigInteger.ZERO
: sysApiKey.getDeptId());
account.setLoginName("apikey:" + sysApiKey.getId());
account.setNickname("API 调用方");
return account;
}
}

View File

@@ -16,7 +16,10 @@ public class PublicApiConfig implements WebMvcConfigurer {
registry.addInterceptor(publicApiInterceptor)
.addPathPatterns("/public-api/**")
.excludePathPatterns("/public-api/bot/chat")
.excludePathPatterns(
"/public-api/agent/chat",
"/public-api/bot/chat"
)
;
}
}

View File

@@ -0,0 +1,151 @@
package tech.easyflow.publicapi.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.agent.runtime.AgentChatRequest;
import tech.easyflow.agent.runtime.AgentRunService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysApiKey;
import tech.easyflow.system.service.SysApiKeyService;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* {@link PublicAgentController} API Key 身份边界测试。
*/
public class PublicAgentControllerTest {
/**
* 验证缺少 API Key 时返回明确的 HTTP 401 业务异常。
*/
@Test
public void chatShouldRejectMissingApiKey() {
PublicAgentController controller = new PublicAgentController(
new RecordingAgentRunService(new ArrayList<>()),
proxy(
SysApiKeyService.class,
(instance, method, args) -> {
throw new AssertionError("缺少 API Key 时不应调用服务");
}
)
);
HttpServletRequest request = proxy(
HttpServletRequest.class,
(instance, method, args) -> {
if ("getHeader".equals(method.getName())) {
return " ";
}
throw new AssertionError("测试路径不应调用 HttpServletRequest." + method.getName());
}
);
BusinessException exception = Assert.assertThrows(
BusinessException.class,
() -> controller.chat(new AgentChatRequest(), request)
);
Assert.assertEquals(401, exception.getHttpStatus());
Assert.assertEquals("Apikey不能为空!", exception.getMessage());
}
/**
* 验证接口先校验 API Key并将其租户与部门传入 Agent 运行时。
*/
@Test
public void chatShouldAuthorizeBeforeRunningWithApiKeyTenant() {
List<String> calls = new ArrayList<>();
SysApiKey apiKey = new SysApiKey();
apiKey.setId(BigInteger.valueOf(101));
apiKey.setTenantId(BigInteger.valueOf(201));
apiKey.setDeptId(BigInteger.valueOf(301));
SysApiKeyService apiKeyService = proxy(
SysApiKeyService.class,
(instance, method, args) -> {
if ("checkApikeyPermission".equals(method.getName())) {
calls.add("permission");
Assert.assertEquals("test-key", args[0]);
Assert.assertEquals("/public-api/agent/chat", args[1]);
return null;
}
if ("getSysApiKey".equals(method.getName())) {
calls.add("load");
return apiKey;
}
throw new AssertionError("测试路径不应调用 SysApiKeyService." + method.getName());
}
);
RecordingAgentRunService runService = new RecordingAgentRunService(calls);
PublicAgentController controller = new PublicAgentController(runService, apiKeyService);
HttpServletRequest request = proxy(
HttpServletRequest.class,
(instance, method, args) -> {
if ("getHeader".equals(method.getName())) {
return "test-key";
}
if ("getRequestURI".equals(method.getName())) {
return "/public-api/agent/chat";
}
throw new AssertionError("测试路径不应调用 HttpServletRequest." + method.getName());
}
);
SseEmitter result = controller.chat(new AgentChatRequest(), request);
Assert.assertSame(runService.emitter, result);
Assert.assertEquals(List.of("permission", "load", "chat"), calls);
Assert.assertEquals(apiKey.getId(), runService.account.getId());
Assert.assertEquals(apiKey.getTenantId(), runService.account.getTenantId());
Assert.assertEquals(apiKey.getDeptId(), runService.account.getDeptId());
}
/**
* 创建接口代理。
*
* @param type 接口类型
* @param handler 调用处理器
* @param <T> 接口类型
* @return 代理实例
*/
private <T> T proxy(Class<T> type, java.lang.reflect.InvocationHandler handler) {
return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handler));
}
/**
* 记录公共 Agent 调用身份的运行服务。
*/
private static class RecordingAgentRunService extends AgentRunService {
private final List<String> calls;
private final SseEmitter emitter = new SseEmitter();
private LoginAccount account;
/**
* 创建运行服务桩。
*
* @param calls 调用顺序记录
*/
private RecordingAgentRunService(List<String> calls) {
this.calls = calls;
}
/**
* 记录 API Key 调用身份。
*
* @param chatRequest 聊天请求
* @param apiAccount API Key 调用身份
* @return 测试用 SSE Emitter
*/
@Override
public SseEmitter chatPublic(AgentChatRequest chatRequest, LoginAccount apiAccount) {
calls.add("chat");
account = apiAccount;
return emitter;
}
}
}

View File

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

View File

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

View File

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

View File

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