发布 v1.10 #5
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -230,6 +231,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return SSE Emitter
|
||||
*/
|
||||
@PostMapping("chat")
|
||||
@SaCheckPermission("/api/v1/agent/session/query")
|
||||
public SseEmitter chat(@JsonBody AgentChatRequest request) {
|
||||
return agentRunService.chat(request);
|
||||
}
|
||||
@@ -241,6 +243,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return SSE Emitter
|
||||
*/
|
||||
@PostMapping("/chat/draft")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public SseEmitter chatDraft(@JsonBody AgentDraftChatRequest request) {
|
||||
return agentRunService.chatDraft(request);
|
||||
}
|
||||
@@ -255,6 +258,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 上传结果
|
||||
*/
|
||||
@PostMapping(value = "/media/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentMediaUploadView> uploadMedia(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("mode") String mode,
|
||||
@RequestParam("agentId") String agentId,
|
||||
@@ -269,6 +275,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/media/delete")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> deleteMedia(@JsonBody(value = "uploadId", required = true) String uploadId) {
|
||||
agentMediaService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
|
||||
return Result.ok();
|
||||
@@ -281,6 +290,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 图片响应
|
||||
*/
|
||||
@GetMapping("/media/content")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
@LogReporterDisabled
|
||||
public ResponseEntity<byte[]> mediaContent(@RequestParam("reference") String reference) {
|
||||
AgentMediaResource resource = agentMediaService.load(reference, SaTokenUtil.getLoginAccount());
|
||||
@@ -303,6 +315,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 上传与读取状态
|
||||
*/
|
||||
@PostMapping(value = "/media/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentDocumentUploadView> uploadDocument(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("mode") String mode,
|
||||
@RequestParam("agentId") String agentId,
|
||||
@@ -320,6 +335,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 最新状态
|
||||
*/
|
||||
@GetMapping("/media/document/status")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentDocumentUploadView> documentStatus(@RequestParam("uploadId") String uploadId) {
|
||||
return Result.ok(agentDocumentService.status(uploadId, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
@@ -331,6 +349,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 重试后的状态
|
||||
*/
|
||||
@PostMapping("/media/document/retry")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentDocumentUploadView> retryDocument(
|
||||
@JsonBody(value = "uploadId", required = true) String uploadId) {
|
||||
return Result.ok(agentDocumentService.retry(uploadId, SaTokenUtil.getLoginAccount()));
|
||||
@@ -343,6 +364,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/media/document/delete")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> deleteDocument(
|
||||
@JsonBody(value = "uploadId", required = true) String uploadId) {
|
||||
agentDocumentService.deleteUpload(uploadId, SaTokenUtil.getLoginAccount());
|
||||
@@ -356,6 +380,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 文档流
|
||||
*/
|
||||
@GetMapping("/media/document/content")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
@LogReporterDisabled
|
||||
public ResponseEntity<StreamingResponseBody> documentContent(
|
||||
@RequestParam("reference") String reference) {
|
||||
@@ -384,6 +411,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 会话信息
|
||||
*/
|
||||
@PostMapping("/composer/session")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentComposerSession> allocateComposerSession(
|
||||
@JsonBody(value = "mode", required = true) String mode) {
|
||||
return Result.ok(agentComposerDraftService.allocateSession(mode));
|
||||
@@ -396,6 +426,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 保存后的草稿
|
||||
*/
|
||||
@PostMapping("/composer/draft/persist")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentComposerDraft> saveComposerDraft(@JsonBody AgentComposerDraft draft) {
|
||||
return Result.ok(agentComposerDraftService.save(draft, SaTokenUtil.getLoginAccount()));
|
||||
}
|
||||
@@ -409,6 +442,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 输入草稿
|
||||
*/
|
||||
@GetMapping("/composer/draft")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<AgentComposerDraft> getComposerDraft(@RequestParam("mode") String mode,
|
||||
@RequestParam("agentId") String agentId,
|
||||
@RequestParam(value = "sessionId", required = false) String sessionId) {
|
||||
@@ -428,6 +464,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/composer/draft/delete")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> deleteComposerDraft(@JsonBody(value = "mode", required = true) String mode,
|
||||
@JsonBody(value = "agentId", required = true) String agentId,
|
||||
@JsonBody(value = "sessionId", required = true) String sessionId,
|
||||
@@ -446,6 +485,7 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/chat/draft/clear")
|
||||
@SaCheckPermission("/api/v1/agent/save")
|
||||
public Result<Void> clearDraftSession(@JsonBody(value = "sessionId", required = true) String sessionId) {
|
||||
agentRunService.clearDraftSession(sessionId);
|
||||
return Result.ok();
|
||||
@@ -459,6 +499,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/run/approve")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> approve(@JsonBody("requestId") String requestId,
|
||||
@JsonBody(value = "resumeToken", required = true) String resumeToken) {
|
||||
agentRunService.approve(requestId, resumeToken);
|
||||
@@ -474,6 +517,9 @@ public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/run/reject")
|
||||
@SaCheckPermission(value = {
|
||||
"/api/v1/agent/session/query", "/api/v1/agent/save"
|
||||
}, mode = SaMode.OR)
|
||||
public Result<Void> reject(@JsonBody("requestId") String requestId,
|
||||
@JsonBody(value = "resumeToken", required = true) String resumeToken,
|
||||
@JsonBody("reason") String reason) {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package tech.easyflow.admin.controller.agent;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||
import tech.easyflow.admin.service.agent.AgentSessionService;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.agent.vo.AgentResourceOptionsView;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -22,17 +26,42 @@ import java.util.List;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/agent/session")
|
||||
@SaCheckPermission("/api/v1/agent/session/query")
|
||||
public class AgentSessionController {
|
||||
|
||||
private final AgentSessionService agentSessionService;
|
||||
private final AgentOptionQueryService agentOptionQueryService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 管理端会话控制器。
|
||||
*
|
||||
* @param agentSessionService Agent 会话服务
|
||||
* @param agentOptionQueryService Agent 安全选项服务
|
||||
*/
|
||||
public AgentSessionController(AgentSessionService agentSessionService) {
|
||||
public AgentSessionController(AgentSessionService agentSessionService,
|
||||
AgentOptionQueryService agentOptionQueryService) {
|
||||
this.agentSessionService = agentSessionService;
|
||||
this.agentOptionQueryService = agentOptionQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询正式聊天可使用的已发布 Agent。
|
||||
*
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
@GetMapping("/options")
|
||||
public Result<List<AgentOptionView>> options() {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询正式聊天可附加的知识库。
|
||||
*
|
||||
* @return 知识库安全选项
|
||||
*/
|
||||
@GetMapping("/knowledgeOptions")
|
||||
public Result<List<AgentResourceOptionsView.ResourceOption>> knowledgeOptions() {
|
||||
return Result.ok(agentOptionQueryService.listKnowledgeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
@@ -23,21 +26,36 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/chatHistory")
|
||||
@SaCheckPermission("/api/v1/chatHistory/query")
|
||||
public class ChatHistoryController {
|
||||
|
||||
private final ChatHistoryManageService chatHistoryManageService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final AgentOptionQueryService agentOptionQueryService;
|
||||
|
||||
/**
|
||||
* 创建聊天历史控制器。
|
||||
*
|
||||
* @param chatHistoryManageService 聊天历史管理服务
|
||||
* @param categoryPermissionService 账号权限服务
|
||||
* @param agentOptionQueryService Agent 安全选项服务
|
||||
*/
|
||||
public ChatHistoryController(ChatHistoryManageService chatHistoryManageService,
|
||||
CategoryPermissionService categoryPermissionService) {
|
||||
CategoryPermissionService categoryPermissionService,
|
||||
AgentOptionQueryService agentOptionQueryService) {
|
||||
this.chatHistoryManageService = chatHistoryManageService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
this.agentOptionQueryService = agentOptionQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天记录筛选可使用的 Agent。
|
||||
*
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
@GetMapping("/agentOptions")
|
||||
public Result<List<AgentOptionView>> agentOptions() {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,10 @@ 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.admin.model.ai.ModelGatewayConfigView;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.entity.SysOption;
|
||||
import tech.easyflow.system.service.SysOptionService;
|
||||
import tech.easyflow.ai.service.capability.ModelCapabilityResolution;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
@@ -40,6 +44,11 @@ import java.util.stream.Collectors;
|
||||
@RequestMapping("/api/v1/model")
|
||||
public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
|
||||
private static final String CHAT_PUBLISH_BASE_URL = "chat_publish_base_url";
|
||||
|
||||
@Autowired
|
||||
private SysOptionService sysOptionService;
|
||||
|
||||
public ModelController(ModelService service) {
|
||||
super(service);
|
||||
}
|
||||
@@ -47,6 +56,21 @@ public class ModelController extends BaseCurdController<ModelService, Model> {
|
||||
@Autowired
|
||||
ModelService modelService;
|
||||
|
||||
/**
|
||||
* 查询模型统一网关页面所需的安全配置。
|
||||
*
|
||||
* @return 仅包含模型发布基础地址的配置
|
||||
*/
|
||||
@GetMapping("/gatewayConfig")
|
||||
@SaCheckPermission("/api/v1/model/query")
|
||||
public Result<ModelGatewayConfigView> gatewayConfig() {
|
||||
SysOption option = sysOptionService.getByOptionKey(
|
||||
CHAT_PUBLISH_BASE_URL,
|
||||
SaTokenUtil.getLoginAccount().getTenantId()
|
||||
);
|
||||
return Result.ok(new ModelGatewayConfigView(option == null ? null : option.getValue()));
|
||||
}
|
||||
|
||||
@Resource
|
||||
ModelMapper modelMapper;
|
||||
@Resource
|
||||
|
||||
@@ -1,88 +1,45 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.node.ConfirmNode;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.admin.service.ai.WorkflowDesignerOptionService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 工作流节点兼容接口。
|
||||
*/
|
||||
@RequestMapping("/api/v1/workflowNode")
|
||||
@RestController
|
||||
public class WorkFlowNodeController {
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
private final WorkflowDesignerOptionService workflowDesignerOptionService;
|
||||
|
||||
@GetMapping("/getChainParams")
|
||||
public Result<?> getChainParams(String currentId, String workflowId) {
|
||||
if (workflowId.equals(currentId)) {
|
||||
throw new BusinessException("工作流不能作为自身子节点");
|
||||
}
|
||||
JSONObject nodeData = new JSONObject();
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在: " + workflowId);
|
||||
}
|
||||
nodeData.put("workflowId", workflow.getId());
|
||||
nodeData.put("workflowName", workflow.getTitle());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
List<Node> nodes = definition.getNodes();
|
||||
JSONArray inputs = new JSONArray();
|
||||
JSONArray outputs = new JSONArray();
|
||||
for (Node node : nodes) {
|
||||
if (node instanceof StartNode) {
|
||||
inputs = JSON.parseArray(JSON.toJSONString(node.getParameters()));
|
||||
handleArray(inputs);
|
||||
}
|
||||
if (node instanceof EndNode) {
|
||||
outputs = JSON.parseArray(JSON.toJSONString(((EndNode) node).getOutputDefs()));
|
||||
handleArray(outputs);
|
||||
}
|
||||
if (node instanceof ConfirmNode) {
|
||||
throw new BusinessException("工作流存在【确认节点】,暂不支持作为子节点");
|
||||
}
|
||||
}
|
||||
nodeData.put("parameters", inputs);
|
||||
nodeData.put("outputDefs", outputs);
|
||||
return Result.ok(nodeData);
|
||||
/**
|
||||
* 创建工作流节点兼容控制器。
|
||||
*
|
||||
* @param workflowDesignerOptionService 工作流设计器选项服务
|
||||
*/
|
||||
public WorkFlowNodeController(WorkflowDesignerOptionService workflowDesignerOptionService) {
|
||||
this.workflowDesignerOptionService = workflowDesignerOptionService;
|
||||
}
|
||||
|
||||
private void handleArray(JSONArray array) {
|
||||
if (array != null) {
|
||||
for (Object o : array) {
|
||||
JSONObject obj = (JSONObject) o;
|
||||
obj.put("id", IdUtil.simpleUUID());
|
||||
obj.put("nameDisabled", true);
|
||||
obj.put("dataTypeDisabled", true);
|
||||
obj.put("deleteDisabled", true);
|
||||
obj.put("addChildDisabled", true);
|
||||
obj.put("refType", "ref");
|
||||
JSONArray children = obj.getJSONArray("children");
|
||||
if (children != null) {
|
||||
handleArray(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 查询子流程输入输出定义。
|
||||
*
|
||||
* @param currentId 当前工作流 ID
|
||||
* @param workflowId 子流程 ID
|
||||
* @return 子流程节点配置
|
||||
* @deprecated 请使用 {@code /api/v1/workflow/designer/childWorkflow}
|
||||
*/
|
||||
@Deprecated
|
||||
@GetMapping("/getChainParams")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<JSONObject> getChainParams(BigInteger currentId, BigInteger workflowId) {
|
||||
return Result.ok(workflowDesignerOptionService.getChildWorkflowNodeData(currentId, workflowId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
@@ -14,6 +15,8 @@ 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.admin.model.ai.WorkflowDesignerOptionsView;
|
||||
import tech.easyflow.admin.service.ai.WorkflowDesignerOptionService;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
@@ -43,6 +46,7 @@ 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 tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
@@ -101,12 +105,116 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
@Resource
|
||||
private WorkflowShareResourceAccessGrantProvider workflowShareGrantProvider;
|
||||
@Resource
|
||||
private WorkflowDesignerOptionService workflowDesignerOptionService;
|
||||
|
||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||
super(service);
|
||||
this.modelService = modelService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流设计器初始化所需的安全选项。
|
||||
*
|
||||
* @return 模型、知识库和代码引擎选项
|
||||
*/
|
||||
@GetMapping("/designer/options")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<WorkflowDesignerOptionsView> designerOptions() {
|
||||
return Result.ok(workflowDesignerOptionService.listOptions(
|
||||
codeEngineCapabilityService.listSupportedCodeEngines()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询工作流设计器可用插件。
|
||||
*
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 插件安全选项分页
|
||||
*/
|
||||
@GetMapping("/designer/plugins")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<Page<WorkflowDesignerOptionsView.PluginOption>> designerPlugins(
|
||||
Long pageNumber,
|
||||
Long pageSize) {
|
||||
return Result.ok(workflowDesignerOptionService.pagePlugins(pageNumber, pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流插件节点配置。
|
||||
*
|
||||
* @param id 插件工具 ID
|
||||
* @return 插件节点配置
|
||||
*/
|
||||
@GetMapping("/designer/pluginTinyFlow")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<?> designerPluginTinyFlow(BigInteger id) {
|
||||
return Result.ok(workflowDesignerOptionService.getPluginTinyFlowData(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流设计器的子流程节点配置。
|
||||
*
|
||||
* @param currentId 当前工作流 ID
|
||||
* @param workflowId 子流程 ID
|
||||
* @return 子流程输入输出定义
|
||||
*/
|
||||
@GetMapping("/designer/childWorkflow")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<JSONObject> designerChildWorkflow(BigInteger currentId, BigInteger workflowId) {
|
||||
return Result.ok(workflowDesignerOptionService.getChildWorkflowNodeData(currentId, workflowId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点可见的数据源。
|
||||
*
|
||||
* @return 数据源安全选项
|
||||
*/
|
||||
@GetMapping("/designer/dataSources")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<List<WorkflowDesignerOptionsView.DataSourceOption>> designerDataSources() {
|
||||
return Result.ok(workflowDesignerOptionService.listDataSources());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点的数据目录。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @return 目录安全选项
|
||||
*/
|
||||
@GetMapping("/designer/catalogs")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<List<WorkflowDesignerOptionsView.CatalogOption>> designerCatalogs(BigInteger sourceId) {
|
||||
return Result.ok(workflowDesignerOptionService.listCatalogs(sourceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点的已接入数据集。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogId 目录 ID
|
||||
* @return 数据集安全选项
|
||||
*/
|
||||
@GetMapping("/designer/managedTables")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<List<WorkflowDesignerOptionsView.DatasetOption>> designerManagedTables(
|
||||
BigInteger sourceId,
|
||||
BigInteger catalogId) {
|
||||
return Result.ok(workflowDesignerOptionService.listManagedDatasets(sourceId, catalogId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点的数据集结构。
|
||||
*
|
||||
* @param datasetRef 数据集引用
|
||||
* @return 数据集结构安全视图
|
||||
*/
|
||||
@GetMapping("/designer/schema")
|
||||
@SaCheckPermission("/api/v1/workflow/query")
|
||||
public Result<WorkflowDesignerOptionsView.DatasetSchemaOption> designerSchema(DatasetRef datasetRef) {
|
||||
return Result.ok(workflowDesignerOptionService.getDatasetSchema(datasetRef));
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点单独运行
|
||||
*/
|
||||
@@ -128,6 +236,9 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
if (workflow == null) {
|
||||
return Result.fail(1, "工作流不存在");
|
||||
}
|
||||
workflowCheckService.checkOrThrow(
|
||||
workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
if (variables == null) {
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
@@ -161,6 +272,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
throw new RuntimeException("工作流不存在");
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||
if (StpUtil.isLogin()) {
|
||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
@@ -252,6 +364,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||
if (res == null) {
|
||||
return Result.fail(2, "节点配置错误,请检查! ");
|
||||
@@ -431,6 +544,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
workflow.setAlias(IdUtil.fastSimpleUUID());
|
||||
workflow.setRevision(0);
|
||||
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
||||
workflowDesignerOptionService.assertContentReferences(workflow.getContent());
|
||||
service.save(workflow);
|
||||
return Result.ok();
|
||||
}
|
||||
@@ -497,6 +611,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
}
|
||||
if (StringUtils.hasLength(entity.getContent())) {
|
||||
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
||||
workflowDesignerOptionService.assertContentReferences(entity.getContent());
|
||||
}
|
||||
|
||||
String alias = entity.getAlias();
|
||||
|
||||
@@ -6,6 +6,7 @@ import tech.easyflow.common.dict.Dict;
|
||||
import tech.easyflow.common.dict.DictItem;
|
||||
import tech.easyflow.common.dict.DictLoader;
|
||||
import tech.easyflow.common.dict.DictManager;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -16,18 +17,30 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 提供已注册静态字典的查询接口。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dict/")
|
||||
public class DictController {
|
||||
|
||||
@Resource
|
||||
DictManager dictManager;
|
||||
private DictManager dictManager;
|
||||
|
||||
/**
|
||||
* 查询指定静态字典的选项。
|
||||
*
|
||||
* @param code 字典编码
|
||||
* @param keyword 搜索关键字
|
||||
* @param request HTTP 请求
|
||||
* @return 字典选项
|
||||
* @throws BusinessException 字典未注册时抛出
|
||||
*/
|
||||
@GetMapping("/items/{code}")
|
||||
public Result<List<DictItem>> items(@PathVariable("code") String code, String keyword, HttpServletRequest request) {
|
||||
DictLoader loader = dictManager.getLoader(code);
|
||||
if (loader == null) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
throw new BusinessException("字典不存在或不支持公共查询");
|
||||
}
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
Dict dict = loader.load(keyword, parameterMap);
|
||||
|
||||
@@ -10,6 +10,8 @@ import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.agent.vo.AgentOptionView;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
@@ -28,9 +30,29 @@ import java.util.List;
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
private final AgentOptionQueryService agentOptionQueryService;
|
||||
|
||||
public DashboardController(DashboardService dashboardService) {
|
||||
/**
|
||||
* 创建管理端工作台控制器。
|
||||
*
|
||||
* @param dashboardService 工作台统计服务
|
||||
* @param agentOptionQueryService Agent 安全选项服务
|
||||
*/
|
||||
public DashboardController(DashboardService dashboardService,
|
||||
AgentOptionQueryService agentOptionQueryService) {
|
||||
this.dashboardService = dashboardService;
|
||||
this.agentOptionQueryService = agentOptionQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作台筛选可使用的 Agent。
|
||||
*
|
||||
* @return Agent 安全选项
|
||||
*/
|
||||
@GetMapping("/agentOptions")
|
||||
@SaCheckPermission("/api/v1/dashboard/query")
|
||||
public Result<List<AgentOptionView>> agentOptions() {
|
||||
return Result.ok(agentOptionQueryService.listAgentOptions(false));
|
||||
}
|
||||
|
||||
@GetMapping("/overview")
|
||||
|
||||
@@ -3,6 +3,7 @@ package tech.easyflow.admin.controller.job;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.quartz.CronExpression;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -11,6 +12,9 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.admin.model.SysJobWorkflowOptionView;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
@@ -33,6 +37,7 @@ import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 系统任务表 控制层。
|
||||
@@ -47,6 +52,9 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
/** 工作流服务。 */
|
||||
private final WorkflowService workflowService;
|
||||
|
||||
/** 工作流使用权限校验服务。 */
|
||||
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
|
||||
|
||||
/** 资源访问控制服务。 */
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
@@ -58,15 +66,18 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
*
|
||||
* @param service 定时任务服务
|
||||
* @param workflowService 工作流服务
|
||||
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
|
||||
* @param resourceAccessService 资源访问控制服务
|
||||
* @param workflowRunningParameterResolver 工作流运行参数解析器
|
||||
*/
|
||||
public SysJobController(SysJobService service,
|
||||
WorkflowService workflowService,
|
||||
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
WorkflowRunningParameterResolver workflowRunningParameterResolver) {
|
||||
super(service);
|
||||
this.workflowService = workflowService;
|
||||
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.workflowRunningParameterResolver = workflowRunningParameterResolver;
|
||||
}
|
||||
@@ -75,6 +86,9 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
@LogRecord("启动定时任务")
|
||||
public Result<Void> start(BigInteger id) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
SysJob job = requireExistingJob(id);
|
||||
validateWorkflowReference(job, account);
|
||||
service.startJob(id);
|
||||
return Result.ok();
|
||||
}
|
||||
@@ -88,6 +102,7 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
}
|
||||
|
||||
@GetMapping("/getNextTimes")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
public Result<List<String>> getNextTimes(String cronExpression) throws Exception{
|
||||
CronExpression ex = new CronExpression(cronExpression);
|
||||
List<String> times = new ArrayList<>();
|
||||
@@ -100,16 +115,72 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
return Result.ok(times);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前账号可用于定时任务的工作流。
|
||||
*
|
||||
* @return 工作流安全选项
|
||||
*/
|
||||
@GetMapping("/workflowOptions")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
public Result<List<SysJobWorkflowOptionView>> workflowOptions() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
List<SysJobWorkflowOptionView> options = workflowService.list(QueryWrapper.create()
|
||||
.eq(Workflow::getTenantId, account.getTenantId())
|
||||
.eq(Workflow::getStatus, EnumDataStatus.AVAILABLE.getCode())
|
||||
.orderBy(Workflow::getModified, false))
|
||||
.stream()
|
||||
.filter(workflow -> Objects.equals(workflow.getTenantId(), account.getTenantId()))
|
||||
.filter(workflow -> resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE))
|
||||
.map(workflow -> new SysJobWorkflowOptionView(
|
||||
workflow.getId(),
|
||||
workflow.getTitle(),
|
||||
workflow.getDescription()))
|
||||
.toList();
|
||||
return Result.ok(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询定时任务所选工作流的运行参数。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @return 工作流运行参数
|
||||
* @throws BusinessException 工作流不存在或无运行权限时抛出
|
||||
*/
|
||||
@GetMapping("/workflowRunningParameters")
|
||||
@SaCheckPermission("/api/v1/sysJob/save")
|
||||
public Result<Map<String, Object>> workflowRunningParameters(BigInteger id) {
|
||||
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
id,
|
||||
SaTokenUtil.getLoginAccount(),
|
||||
"工作流不存在、已禁用或无权运行");
|
||||
Map<String, Object> result = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||
if (result == null) {
|
||||
throw new BusinessException("工作流参数配置无效,请检查工作流后重试");
|
||||
}
|
||||
return Result.ok(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(SysJob entity, boolean isSave) {
|
||||
if (entity == null) {
|
||||
throw new BusinessException("定时任务不能为空");
|
||||
}
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
SysJob effectiveEntity = entity;
|
||||
if (isSave) {
|
||||
commonFiled(entity,loginUser.getId(),loginUser.getTenantId(), loginUser.getDeptId());
|
||||
} else {
|
||||
SysJob existing = requireExistingJob(entity.getId());
|
||||
preserveServerControlledFields(entity, existing);
|
||||
effectiveEntity = mergeForValidation(entity, existing);
|
||||
entity.setModified(new Date());
|
||||
entity.setModifiedBy(loginUser.getId());
|
||||
}
|
||||
validateWorkflowReference(entity);
|
||||
validateWorkflowReference(effectiveEntity, loginUser);
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
|
||||
@@ -117,27 +188,71 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
||||
* 校验工作流类型任务引用的工作流可被当前用户运行。
|
||||
*
|
||||
* @param entity 待保存的定时任务
|
||||
* @param account 当前账号
|
||||
* @throws BusinessException 工作流不存在、参数非法或无运行权限时抛出
|
||||
*/
|
||||
private void validateWorkflowReference(SysJob entity) {
|
||||
private void validateWorkflowReference(SysJob entity, LoginAccount account) {
|
||||
if (entity == null
|
||||
|| !Integer.valueOf(EnumJobType.TINY_FLOW.getCode()).equals(entity.getJobType())) {
|
||||
return;
|
||||
}
|
||||
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(entity);
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在,请重新选择");
|
||||
}
|
||||
resourceAccessService.assertAccess(
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE,
|
||||
"无权限运行所选工作流"
|
||||
);
|
||||
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
workflowId,
|
||||
account,
|
||||
"工作流不存在、已禁用或无权运行");
|
||||
validateRequiredWorkflowParams(entity, workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户内存在的定时任务。
|
||||
*
|
||||
* @param id 定时任务 ID
|
||||
* @return 已存在的定时任务
|
||||
* @throws BusinessException ID 缺失或任务不存在时抛出
|
||||
*/
|
||||
private SysJob requireExistingJob(BigInteger id) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("定时任务ID不能为空");
|
||||
}
|
||||
SysJob existing = service.getById(id);
|
||||
if (existing == null) {
|
||||
throw new BusinessException("定时任务不存在");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保留更新请求不能修改的服务端控制字段。
|
||||
*
|
||||
* @param entity 更新请求
|
||||
* @param existing 数据库中的定时任务
|
||||
*/
|
||||
private void preserveServerControlledFields(SysJob entity, SysJob existing) {
|
||||
entity.setTenantId(existing.getTenantId());
|
||||
entity.setDeptId(existing.getDeptId());
|
||||
entity.setCreated(existing.getCreated());
|
||||
entity.setCreatedBy(existing.getCreatedBy());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并部分更新请求与原记录,生成用于权限和参数校验的有效任务状态。
|
||||
*
|
||||
* @param entity 更新请求
|
||||
* @param existing 数据库中的定时任务
|
||||
* @return 合并后的校验对象
|
||||
*/
|
||||
private SysJob mergeForValidation(SysJob entity, SysJob existing) {
|
||||
SysJob effective = new SysJob();
|
||||
effective.setJobType(entity.getJobType() == null
|
||||
? existing.getJobType()
|
||||
: entity.getJobType());
|
||||
effective.setJobParams(entity.getJobParams() == null
|
||||
? existing.getJobParams()
|
||||
: entity.getJobParams());
|
||||
return effective;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验定时任务已填写工作流的全部必填运行参数。
|
||||
*
|
||||
|
||||
@@ -10,6 +10,8 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalAssigneeOptionVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowPageVo;
|
||||
@@ -36,6 +38,20 @@ public class ApprovalFlowController {
|
||||
|
||||
@Resource
|
||||
private ApprovalAssigneeService approvalAssigneeService;
|
||||
@Resource
|
||||
private SystemFormOptionService systemFormOptionService;
|
||||
|
||||
/**
|
||||
* 查询审批流程配置所需的资源范围选项。
|
||||
*
|
||||
* @return 非 Bot 分类和部门树
|
||||
*/
|
||||
@GetMapping("/resourceScopeOptions")
|
||||
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||
public Result<SystemFormOptionsView.ApprovalResourceScopeOptions> resourceScopeOptions() {
|
||||
assertSuperAdmin();
|
||||
return Result.ok(systemFormOptionService.approvalResourceScopeOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询审批流程。
|
||||
@@ -102,6 +118,9 @@ public class ApprovalFlowController {
|
||||
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||
public Result<BigInteger> save(@JsonBody ApprovalFlowDetailVo request) {
|
||||
assertSuperAdmin();
|
||||
systemFormOptionService.validateApprovalScopes(
|
||||
request == null ? null : request.getResourceType(),
|
||||
request == null ? null : request.getScopes());
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
return Result.ok(approvalFlowService.saveFlow(request, operatorId));
|
||||
}
|
||||
@@ -116,6 +135,9 @@ public class ApprovalFlowController {
|
||||
@SaCheckPermission("/api/v1/approvalFlow/save")
|
||||
public Result<Void> update(@JsonBody ApprovalFlowDetailVo request) {
|
||||
assertSuperAdmin();
|
||||
systemFormOptionService.validateApprovalScopes(
|
||||
request == null ? null : request.getResourceType(),
|
||||
request == null ? null : request.getScopes());
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
approvalFlowService.updateFlow(request, operatorId);
|
||||
return Result.ok();
|
||||
|
||||
@@ -31,6 +31,8 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.log.annotation.LogRecord;
|
||||
import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo;
|
||||
import tech.easyflow.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||
@@ -43,8 +45,8 @@ import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -71,6 +73,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
|
||||
private final AuthCredentialKeyService credentialKeyService;
|
||||
private final SysRoleService sysRoleService;
|
||||
private final SystemFormOptionService systemFormOptionService;
|
||||
@Resource
|
||||
private AuthService authService;
|
||||
|
||||
@@ -80,13 +83,16 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
* @param service 用户服务
|
||||
* @param credentialKeyService 凭证密钥服务
|
||||
* @param sysRoleService 角色服务
|
||||
* @param systemFormOptionService 用户表单安全选项服务
|
||||
*/
|
||||
public SysAccountController(SysAccountService service,
|
||||
AuthCredentialKeyService credentialKeyService,
|
||||
SysRoleService sysRoleService) {
|
||||
SysRoleService sysRoleService,
|
||||
SystemFormOptionService systemFormOptionService) {
|
||||
super(service);
|
||||
this.credentialKeyService = credentialKeyService;
|
||||
this.sysRoleService = sysRoleService;
|
||||
this.systemFormOptionService = systemFormOptionService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,6 +194,7 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
|
||||
@Override
|
||||
protected Result onSaveOrUpdateBefore(SysAccount entity, boolean isSave) {
|
||||
systemFormOptionService.validateAccountReferences(entity);
|
||||
LoginAccount loginUser = SaTokenUtil.getLoginAccount();
|
||||
if (isSave) {
|
||||
// 查询用户名是否存在
|
||||
@@ -233,6 +240,17 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询账号表单所需的部门、角色和岗位选项。
|
||||
*
|
||||
* @return 账号表单安全选项
|
||||
*/
|
||||
@GetMapping("/formOptions")
|
||||
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||
public Result<SystemFormOptionsView.AccountFormOptions> formOptions() {
|
||||
return Result.ok(systemFormOptionService.accountFormOptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充账号创建的公共字段。
|
||||
*
|
||||
|
||||
@@ -9,6 +9,7 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeDetailVo;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysRoleCategoryScopeService;
|
||||
@@ -25,6 +26,8 @@ public class SysRoleCategoryScopeController {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private SystemFormOptionService systemFormOptionService;
|
||||
|
||||
@GetMapping("/detail")
|
||||
@SaCheckPermission("/api/v1/sysRole/query")
|
||||
@@ -41,6 +44,7 @@ public class SysRoleCategoryScopeController {
|
||||
if (request == null || request.getRoleId() == null) {
|
||||
throw new BusinessException("角色ID不能为空");
|
||||
}
|
||||
systemFormOptionService.validateCategoryScopes(request.getScopes());
|
||||
BigInteger operatorId = SaTokenUtil.getLoginAccount().getId();
|
||||
sysRoleCategoryScopeService.saveRoleScopes(request.getRoleId(), request.getScopes(), operatorId);
|
||||
return Result.ok();
|
||||
|
||||
@@ -9,6 +9,8 @@ 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.jsonbody.JsonBody;
|
||||
import tech.easyflow.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.SysRoleDept;
|
||||
import tech.easyflow.system.entity.SysRoleMenu;
|
||||
@@ -38,11 +40,24 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
||||
private SysRoleMenuService sysRoleMenuService;
|
||||
@Resource
|
||||
private SysRoleDeptService sysRoleDeptService;
|
||||
@Resource
|
||||
private SystemFormOptionService systemFormOptionService;
|
||||
|
||||
public SysRoleController(SysRoleService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色表单所需的菜单和非 Bot 分类选项。
|
||||
*
|
||||
* @return 角色表单安全选项
|
||||
*/
|
||||
@GetMapping("formOptions")
|
||||
@SaCheckPermission("/api/v1/sysRole/query")
|
||||
public Result<SystemFormOptionsView.RoleFormOptions> formOptions() {
|
||||
return Result.ok(systemFormOptionService.roleFormOptions());
|
||||
}
|
||||
|
||||
@PostMapping("saveRoleMenu/{roleId}")
|
||||
@SaCheckPermission("/api/v1/sysRole/save")
|
||||
@Deprecated
|
||||
@@ -85,6 +100,7 @@ public class SysRoleController extends BaseCurdController<SysRoleService, SysRol
|
||||
if (entity.getId() == null) {
|
||||
commonFiled(entity, loginUser.getId(), loginUser.getTenantId(), loginUser.getDeptId());
|
||||
}
|
||||
systemFormOptionService.validateRoleReferences(entity);
|
||||
service.saveRole(entity);
|
||||
return Result.ok(entity.getId());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.admin.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 定时任务可运行的工作流安全选项。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param title 工作流标题
|
||||
* @param description 工作流描述
|
||||
*/
|
||||
public record SysJobWorkflowOptionView(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String title,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package tech.easyflow.admin.model;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理端系统表单所需的安全选项视图。
|
||||
*/
|
||||
public final class SystemFormOptionsView {
|
||||
|
||||
private SystemFormOptionsView() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批流程资源范围选项。
|
||||
*
|
||||
* @param categories 按资源类型分组的分类选项
|
||||
* @param departments 部门树
|
||||
*/
|
||||
public record ApprovalResourceScopeOptions(
|
||||
Map<String, List<CategoryOption>> categories,
|
||||
List<DepartmentOption> departments
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色表单选项。
|
||||
*
|
||||
* @param menus 菜单树
|
||||
* @param categories 按资源类型分组的非 Bot 分类选项
|
||||
*/
|
||||
public record RoleFormOptions(
|
||||
List<MenuOption> menus,
|
||||
Map<String, List<CategoryOption>> categories
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号表单选项。
|
||||
*
|
||||
* @param departments 部门树
|
||||
* @param roles 可用角色
|
||||
* @param positions 可用岗位
|
||||
*/
|
||||
public record AccountFormOptions(
|
||||
List<DepartmentOption> departments,
|
||||
List<RoleOption> roles,
|
||||
List<PositionOption> positions
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类安全选项。
|
||||
*
|
||||
* @param id 分类 ID
|
||||
* @param categoryName 分类名称
|
||||
*/
|
||||
public record CategoryOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String categoryName
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门树安全选项。
|
||||
*
|
||||
* @param id 部门 ID
|
||||
* @param parentId 上级部门 ID
|
||||
* @param deptName 部门名称
|
||||
* @param children 下级部门
|
||||
*/
|
||||
public record DepartmentOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger parentId,
|
||||
String deptName,
|
||||
List<DepartmentOption> children
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单树安全选项。
|
||||
*
|
||||
* @param id 菜单 ID
|
||||
* @param parentId 上级菜单 ID
|
||||
* @param menuTitle 菜单标题
|
||||
* @param children 下级菜单
|
||||
*/
|
||||
public record MenuOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger parentId,
|
||||
String menuTitle,
|
||||
List<MenuOption> children
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色安全选项。
|
||||
*
|
||||
* @param id 角色 ID
|
||||
* @param roleName 角色名称
|
||||
*/
|
||||
public record RoleOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String roleName
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 岗位安全选项。
|
||||
*
|
||||
* @param id 岗位 ID
|
||||
* @param positionName 岗位名称
|
||||
*/
|
||||
public record PositionOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String positionName
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.admin.model.ai;
|
||||
|
||||
/**
|
||||
* 模型统一网关页面所需的安全配置。
|
||||
*
|
||||
* @param publishBaseUrl 模型发布基础地址
|
||||
*/
|
||||
public record ModelGatewayConfigView(String publishBaseUrl) {
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package tech.easyflow.admin.model.ai;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工作流设计器所需的安全选项视图。
|
||||
*
|
||||
* @param models 模型选项
|
||||
* @param knowledges 知识库选项
|
||||
* @param codeEngines 代码执行引擎选项
|
||||
*/
|
||||
public record WorkflowDesignerOptionsView(
|
||||
List<ModelOption> models,
|
||||
List<KnowledgeOption> knowledges,
|
||||
List<Map<String, Object>> codeEngines
|
||||
) {
|
||||
|
||||
/**
|
||||
* 模型安全选项。
|
||||
*
|
||||
* @param id 模型 ID
|
||||
* @param title 模型标题
|
||||
* @param description 模型描述
|
||||
* @param modelProvider 供应商安全摘要
|
||||
*/
|
||||
public record ModelOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String title,
|
||||
String description,
|
||||
ProviderOption modelProvider
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型供应商安全摘要。
|
||||
*
|
||||
* @param providerName 供应商名称
|
||||
* @param providerType 供应商类型
|
||||
* @param icon 供应商图标
|
||||
*/
|
||||
public record ProviderOption(String providerName, String providerType, String icon) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库安全选项。
|
||||
*
|
||||
* @param id 知识库 ID
|
||||
* @param title 知识库标题
|
||||
* @param description 知识库描述
|
||||
*/
|
||||
public record KnowledgeOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String title,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件安全选项。
|
||||
*
|
||||
* @param id 插件 ID
|
||||
* @param name 插件名称
|
||||
* @param description 插件描述
|
||||
* @param icon 插件图标
|
||||
* @param tools 可用工具
|
||||
*/
|
||||
public record PluginOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
List<PluginToolOption> tools
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件工具安全选项。
|
||||
*
|
||||
* @param id 工具 ID
|
||||
* @param name 工具名称
|
||||
* @param description 工具描述
|
||||
*/
|
||||
public record PluginToolOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String name,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据源安全选项。
|
||||
*
|
||||
* @param id 数据源 ID
|
||||
* @param sourceName 数据源名称
|
||||
* @param sourceType 数据源类型
|
||||
*/
|
||||
public record DataSourceOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
String sourceName,
|
||||
String sourceType
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据目录安全选项。
|
||||
*
|
||||
* @param id 目录 ID
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogName 目录名称
|
||||
* @param catalogDesc 目录描述
|
||||
*/
|
||||
public record CatalogOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger sourceId,
|
||||
String catalogName,
|
||||
String catalogDesc
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 已接入数据集安全选项。
|
||||
*
|
||||
* @param id 数据集 ID
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogId 目录 ID
|
||||
* @param tableName 数据表名称
|
||||
* @param tableDesc 数据表描述
|
||||
*/
|
||||
public record DatasetOption(
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger id,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger sourceId,
|
||||
@JsonSerialize(using = ToStringSerializer.class) BigInteger catalogId,
|
||||
String tableName,
|
||||
String tableDesc
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据集字段安全视图。
|
||||
*
|
||||
* @param fieldName 字段名称
|
||||
* @param fieldDesc 字段描述
|
||||
* @param jdbcType JDBC 类型
|
||||
* @param fieldType 业务字段类型
|
||||
*/
|
||||
public record DatasetFieldOption(
|
||||
String fieldName,
|
||||
String fieldDesc,
|
||||
String jdbcType,
|
||||
Integer fieldType
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据集结构安全视图。
|
||||
*
|
||||
* @param tableName 数据表名称
|
||||
* @param tableDesc 数据表描述
|
||||
* @param fields 字段列表
|
||||
*/
|
||||
public record DatasetSchemaOption(
|
||||
String tableName,
|
||||
String tableDesc,
|
||||
List<DatasetFieldOption> fields
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
package tech.easyflow.admin.service.ai;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.Node;
|
||||
import com.easyagents.flow.core.node.ConfirmNode;
|
||||
import com.easyagents.flow.core.node.EndNode;
|
||||
import com.easyagents.flow.core.node.StartNode;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.admin.model.ai.WorkflowDesignerOptionsView;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
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.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.entity.DatacenterTable;
|
||||
import tech.easyflow.datacenter.entity.DatacenterTableField;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
|
||||
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
|
||||
import tech.easyflow.datacenter.meta.enums.DatacenterSourceType;
|
||||
import tech.easyflow.datacenter.meta.model.DatacenterCatalogMeta;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterSourceService;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 查询工作流设计器所需的场景化安全选项。
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowDesignerOptionService {
|
||||
|
||||
private final ModelService modelService;
|
||||
private final DocumentCollectionService documentCollectionService;
|
||||
private final PluginService pluginService;
|
||||
private final PluginItemService pluginItemService;
|
||||
private final PluginVisibilityService pluginVisibilityService;
|
||||
private final WorkflowService workflowService;
|
||||
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
|
||||
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||
private final ChainParser chainParser;
|
||||
private final WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final DatacenterSourceService datacenterSourceService;
|
||||
private final DatacenterDatasetRegistryService datacenterDatasetRegistryService;
|
||||
private final DatacenterDatasetQueryService datacenterDatasetQueryService;
|
||||
|
||||
/**
|
||||
* 创建工作流设计器选项服务。
|
||||
*
|
||||
* @param modelService 模型服务
|
||||
* @param documentCollectionService 知识库服务
|
||||
* @param pluginService 插件服务
|
||||
* @param pluginItemService 插件工具服务
|
||||
* @param pluginVisibilityService 插件可见性服务
|
||||
* @param workflowService 工作流服务
|
||||
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
|
||||
* @param workflowPluginSnapshotResolver 工作流插件快照解析器
|
||||
* @param chainParser 工作流解析器
|
||||
* @param workflowDatacenterContentService 工作流数据中心内容服务
|
||||
* @param resourceAccessService 资源访问服务
|
||||
* @param datacenterSourceService 数据源服务
|
||||
* @param datacenterDatasetRegistryService 数据集注册服务
|
||||
* @param datacenterDatasetQueryService 数据集查询服务
|
||||
*/
|
||||
public WorkflowDesignerOptionService(
|
||||
ModelService modelService,
|
||||
DocumentCollectionService documentCollectionService,
|
||||
PluginService pluginService,
|
||||
PluginItemService pluginItemService,
|
||||
PluginVisibilityService pluginVisibilityService,
|
||||
WorkflowService workflowService,
|
||||
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
|
||||
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
||||
ChainParser chainParser,
|
||||
WorkflowDatacenterContentService workflowDatacenterContentService,
|
||||
ResourceAccessService resourceAccessService,
|
||||
DatacenterSourceService datacenterSourceService,
|
||||
DatacenterDatasetRegistryService datacenterDatasetRegistryService,
|
||||
DatacenterDatasetQueryService datacenterDatasetQueryService) {
|
||||
this.modelService = modelService;
|
||||
this.documentCollectionService = documentCollectionService;
|
||||
this.pluginService = pluginService;
|
||||
this.pluginItemService = pluginItemService;
|
||||
this.pluginVisibilityService = pluginVisibilityService;
|
||||
this.workflowService = workflowService;
|
||||
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
|
||||
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||
this.chainParser = chainParser;
|
||||
this.workflowDatacenterContentService = workflowDatacenterContentService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.datacenterSourceService = datacenterSourceService;
|
||||
this.datacenterDatasetRegistryService = datacenterDatasetRegistryService;
|
||||
this.datacenterDatasetQueryService = datacenterDatasetQueryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询设计器初始化所需的模型和知识库。
|
||||
*
|
||||
* @param codeEngines 代码执行引擎选项
|
||||
* @return 设计器初始化选项
|
||||
*/
|
||||
public WorkflowDesignerOptionsView listOptions(List<Map<String, Object>> codeEngines) {
|
||||
LoginAccount account = requireAccount();
|
||||
return new WorkflowDesignerOptionsView(
|
||||
listModelOptions(account),
|
||||
listKnowledgeOptions(account),
|
||||
codeEngines == null ? List.of() : codeEngines
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工作流内容引用的场景资源是否仍允许当前账号使用。
|
||||
*
|
||||
* @param content 工作流内容
|
||||
* @throws BusinessException 内容引用越权、跨租户或资源失效时抛出
|
||||
*/
|
||||
public void assertContentReferences(String content) {
|
||||
if (content == null || content.isBlank()) {
|
||||
return;
|
||||
}
|
||||
JSONObject root;
|
||||
try {
|
||||
root = JSON.parseObject(content);
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException("工作流内容不是合法JSON");
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LoginAccount account = requireAccount();
|
||||
Set<BigInteger> modelIds = new HashSet<>();
|
||||
Set<BigInteger> knowledgeIds = new HashSet<>();
|
||||
Set<BigInteger> checkedPluginItemIds = new HashSet<>();
|
||||
Set<BigInteger> checkedWorkflowIds = new HashSet<>();
|
||||
Set<BigInteger> checkedSourceIds = new HashSet<>();
|
||||
Set<String> checkedTableIds = new HashSet<>();
|
||||
|
||||
for (int index = 0; index < nodes.size(); index++) {
|
||||
JSONObject node = nodes.getJSONObject(index);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
String nodeType = data.getString("type");
|
||||
if (nodeType == null || nodeType.isBlank()) {
|
||||
nodeType = node.getString("type");
|
||||
}
|
||||
if ("llmNode".equals(nodeType)) {
|
||||
addReferenceId(modelIds, readReferenceId(data, "llmId", "模型"));
|
||||
} else if ("knowledgeNode".equals(nodeType)) {
|
||||
addReferenceId(knowledgeIds, readReferenceId(data, "knowledgeId", "知识库"));
|
||||
} else if ("plugin-node".equals(nodeType)) {
|
||||
BigInteger pluginItemId = readReferenceId(data, "pluginId", "插件工具");
|
||||
if (pluginItemId != null && checkedPluginItemIds.add(pluginItemId)) {
|
||||
getPluginTinyFlowData(pluginItemId);
|
||||
}
|
||||
} else if ("workflow-node".equals(nodeType)) {
|
||||
assertWorkflowReference(readReferenceId(data, "workflowId", "子流程"),
|
||||
account, checkedWorkflowIds);
|
||||
}
|
||||
assertDatasetReference(data, account, checkedSourceIds, checkedTableIds);
|
||||
}
|
||||
assertModelReferences(modelIds, account);
|
||||
assertKnowledgeReferences(knowledgeIds, account);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前账号可用于工作流的插件。
|
||||
*
|
||||
* @param pageNumber 页码
|
||||
* @param pageSize 每页数量
|
||||
* @return 插件安全选项分页
|
||||
*/
|
||||
public Page<WorkflowDesignerOptionsView.PluginOption> pagePlugins(Long pageNumber, Long pageSize) {
|
||||
LoginAccount account = requireAccount();
|
||||
QueryWrapper wrapper = QueryWrapper.create()
|
||||
.eq(Plugin::getTenantId, account.getTenantId().longValue())
|
||||
.orderBy(Plugin::getCreated, false);
|
||||
List<Plugin> plugins = pluginService.getMapper().selectListWithRelationsByQuery(wrapper);
|
||||
List<Plugin> availablePlugins = pluginService.preparePluginsForCurrentUser(plugins, false, true);
|
||||
List<WorkflowDesignerOptionsView.PluginOption> options = availablePlugins.stream()
|
||||
.map(this::toPluginOption)
|
||||
.toList();
|
||||
long actualPageNumber = pageNumber == null || pageNumber < 1 ? 1L : pageNumber;
|
||||
long actualPageSize = pageSize == null || pageSize < 1 ? 10L : Math.min(pageSize, 100L);
|
||||
int fromIndex = Math.toIntExact(Math.min(options.size(), (actualPageNumber - 1) * actualPageSize));
|
||||
int toIndex = Math.toIntExact(Math.min(options.size(), fromIndex + actualPageSize));
|
||||
return new Page<>(
|
||||
options.subList(fromIndex, toIndex),
|
||||
actualPageNumber,
|
||||
actualPageSize,
|
||||
options.size()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个插件工具的工作流节点安全配置。
|
||||
*
|
||||
* @param pluginItemId 插件工具 ID
|
||||
* @return 节点配置
|
||||
* @throws BusinessException 插件工具不存在或不可用时抛出
|
||||
*/
|
||||
public JSONObject getPluginTinyFlowData(BigInteger pluginItemId) {
|
||||
if (pluginItemId == null) {
|
||||
throw new BusinessException("插件工具不能为空");
|
||||
}
|
||||
PluginItem record = pluginItemService.getById(pluginItemId);
|
||||
if (record == null || !Integer.valueOf(1).equals(record.getStatus())) {
|
||||
throw new BusinessException("插件工具不存在或已禁用");
|
||||
}
|
||||
Plugin plugin = pluginService.getById(record.getPluginId());
|
||||
if (plugin == null) {
|
||||
throw new BusinessException("插件不存在");
|
||||
}
|
||||
LoginAccount account = requireAccount();
|
||||
if (!Objects.equals(plugin.getTenantId(), account.getTenantId().longValue())) {
|
||||
throw new BusinessException("无权限访问插件");
|
||||
}
|
||||
pluginVisibilityService.assertPluginVisible(plugin.getCreatedBy(), plugin.getId(), "无权限访问插件");
|
||||
Plugin preparedPlugin = pluginService.preparePluginForCurrentUser(plugin);
|
||||
if (Boolean.FALSE.equals(preparedPlugin.getAvailable())) {
|
||||
throw new BusinessException(preparedPlugin.getReasonMessage());
|
||||
}
|
||||
|
||||
JSONObject nodeData = new JSONObject();
|
||||
nodeData.put("pluginId", record.getId().toString());
|
||||
nodeData.put("pluginName", record.getName());
|
||||
nodeData.put("pluginType", preparedPlugin.getType());
|
||||
nodeData.put("workflowId", preparedPlugin.getWorkflowId());
|
||||
nodeData.put("workflowTitle", preparedPlugin.getWorkflowTitle());
|
||||
nodeData.put("available", preparedPlugin.getAvailable());
|
||||
nodeData.put("reasonCode", preparedPlugin.getReasonCode());
|
||||
nodeData.put("reasonMessage", preparedPlugin.getReasonMessage());
|
||||
nodeData.put("parameters", parseSchemaArray(record.getInputData()));
|
||||
nodeData.put("outputDefs", parseSchemaArray(record.getOutputData()));
|
||||
nodeData.put("schemaHash", resolveSchemaHash(record, preparedPlugin));
|
||||
return nodeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询子流程节点所需的输入输出定义。
|
||||
*
|
||||
* @param currentWorkflowId 当前工作流 ID
|
||||
* @param childWorkflowId 子流程 ID
|
||||
* @return 子流程节点配置
|
||||
* @throws BusinessException 子流程不存在、不可用或无权使用时抛出
|
||||
*/
|
||||
public JSONObject getChildWorkflowNodeData(
|
||||
BigInteger currentWorkflowId,
|
||||
BigInteger childWorkflowId) {
|
||||
if (childWorkflowId == null) {
|
||||
throw new BusinessException("子流程不能为空");
|
||||
}
|
||||
if (Objects.equals(childWorkflowId, currentWorkflowId)) {
|
||||
throw new BusinessException("工作流不能作为自身子节点");
|
||||
}
|
||||
LoginAccount account = requireAccount();
|
||||
Workflow workflow = workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
childWorkflowId,
|
||||
account,
|
||||
"子流程不存在、已禁用或无权使用");
|
||||
assertContentReferences(workflow.getContent());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(
|
||||
workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
JSONArray inputs = new JSONArray();
|
||||
JSONArray outputs = new JSONArray();
|
||||
for (Node node : definition.getNodes()) {
|
||||
if (node instanceof StartNode) {
|
||||
inputs = JSON.parseArray(JSON.toJSONString(node.getParameters()));
|
||||
decorateChildWorkflowSchemaArray(inputs);
|
||||
}
|
||||
if (node instanceof EndNode endNode) {
|
||||
outputs = JSON.parseArray(JSON.toJSONString(endNode.getOutputDefs()));
|
||||
decorateChildWorkflowSchemaArray(outputs);
|
||||
}
|
||||
if (node instanceof ConfirmNode) {
|
||||
throw new BusinessException("工作流存在【确认节点】,暂不支持作为子节点");
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject nodeData = new JSONObject();
|
||||
nodeData.put("workflowId", workflow.getId());
|
||||
nodeData.put("workflowName", workflow.getTitle());
|
||||
nodeData.put("parameters", inputs);
|
||||
nodeData.put("outputDefs", outputs);
|
||||
return nodeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前租户可用于工作流的数据源安全选项。
|
||||
*
|
||||
* @return 数据源安全选项
|
||||
*/
|
||||
public List<WorkflowDesignerOptionsView.DataSourceOption> listDataSources() {
|
||||
LoginAccount account = requireAccount();
|
||||
datacenterDatasetRegistryService.ensureBuiltinSource(DatacenterSourceType.PROJECT_MYSQL, account);
|
||||
return datacenterSourceService.list(QueryWrapper.create()
|
||||
.eq(DatacenterSource::getTenantId, account.getTenantId())
|
||||
.orderBy(DatacenterSource::getModified, false))
|
||||
.stream()
|
||||
.map(source -> new WorkflowDesignerOptionsView.DataSourceOption(
|
||||
source.getId(), source.getSourceName(), source.getSourceType()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个数据源的目录安全选项。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @return 目录安全选项
|
||||
*/
|
||||
public List<WorkflowDesignerOptionsView.CatalogOption> listCatalogs(BigInteger sourceId) {
|
||||
LoginAccount account = requireAccount();
|
||||
requireTenantSource(sourceId, account);
|
||||
return datacenterSourceService.listCatalogs(sourceId, account).stream()
|
||||
.map(this::toCatalogOption)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询一个数据源目录下已接入的数据集。
|
||||
*
|
||||
* @param sourceId 数据源 ID
|
||||
* @param catalogId 目录 ID
|
||||
* @return 数据集安全选项
|
||||
*/
|
||||
public List<WorkflowDesignerOptionsView.DatasetOption> listManagedDatasets(
|
||||
BigInteger sourceId,
|
||||
BigInteger catalogId) {
|
||||
LoginAccount account = requireAccount();
|
||||
requireTenantSource(sourceId, account);
|
||||
return datacenterDatasetRegistryService.listManagedTables(sourceId, catalogId).stream()
|
||||
.filter(table -> Objects.equals(table.getTenantId(), account.getTenantId()))
|
||||
.filter(table -> Objects.equals(table.getSourceId(), sourceId))
|
||||
.filter(table -> catalogId == null || Objects.equals(table.getCatalogId(), catalogId))
|
||||
.map(this::toDatasetOption)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询工作流数据节点所需的数据集结构。
|
||||
*
|
||||
* @param datasetRef 数据集引用
|
||||
* @return 数据集结构安全视图
|
||||
*/
|
||||
public WorkflowDesignerOptionsView.DatasetSchemaOption getDatasetSchema(DatasetRef datasetRef) {
|
||||
if (datasetRef == null || datasetRef.getSourceId() == null || datasetRef.getTableId() == null) {
|
||||
throw new BusinessException("数据集引用不完整");
|
||||
}
|
||||
LoginAccount account = requireAccount();
|
||||
requireTenantSource(datasetRef.getSourceId(), account);
|
||||
DatacenterTable table = datacenterDatasetRegistryService.getTableWithFields(datasetRef.getTableId());
|
||||
if (table == null
|
||||
|| !Objects.equals(table.getTenantId(), account.getTenantId())
|
||||
|| !Objects.equals(table.getSourceId(), datasetRef.getSourceId())) {
|
||||
throw new BusinessException("数据集不存在或无权访问");
|
||||
}
|
||||
DatacenterSchemaResponse schema = datacenterDatasetQueryService.getSchema(datasetRef);
|
||||
List<DatacenterTableField> fields = schema == null || schema.getFields() == null
|
||||
? Collections.emptyList()
|
||||
: schema.getFields();
|
||||
return new WorkflowDesignerOptionsView.DatasetSchemaOption(
|
||||
table.getTableName(),
|
||||
table.getTableDesc(),
|
||||
fields.stream()
|
||||
.map(field -> new WorkflowDesignerOptionsView.DatasetFieldOption(
|
||||
field.getFieldName(),
|
||||
field.getFieldDesc(),
|
||||
field.getJdbcType(),
|
||||
field.getFieldType()
|
||||
))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
private List<WorkflowDesignerOptionsView.ModelOption> listModelOptions(LoginAccount account) {
|
||||
Model query = new Model();
|
||||
query.setTenantId(account.getTenantId());
|
||||
query.setModelType(Model.MODEL_TYPES[0]);
|
||||
return modelService.listSelectableModels(query, false, "id", "desc").stream()
|
||||
.filter(model -> Objects.equals(model.getTenantId(), account.getTenantId()))
|
||||
.map(model -> {
|
||||
ModelProvider provider = model.getModelProvider();
|
||||
WorkflowDesignerOptionsView.ProviderOption providerOption = provider == null
|
||||
? null
|
||||
: new WorkflowDesignerOptionsView.ProviderOption(
|
||||
provider.getProviderName(),
|
||||
provider.getProviderType(),
|
||||
provider.getIcon()
|
||||
);
|
||||
return new WorkflowDesignerOptionsView.ModelOption(
|
||||
model.getId(),
|
||||
model.getTitle(),
|
||||
model.getDescription(),
|
||||
providerOption
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<WorkflowDesignerOptionsView.KnowledgeOption> listKnowledgeOptions(LoginAccount account) {
|
||||
return documentCollectionService.list(QueryWrapper.create()
|
||||
.eq(DocumentCollection::getTenantId, account.getTenantId())
|
||||
.orderBy(DocumentCollection::getModified, false))
|
||||
.stream()
|
||||
.filter(item -> resourceAccessService.canAccess(
|
||||
account, CategoryResourceType.KNOWLEDGE, item, ResourceAction.USE))
|
||||
.map(item -> new WorkflowDesignerOptionsView.KnowledgeOption(
|
||||
item.getId(), item.getTitle(), item.getDescription()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void addReferenceId(Set<BigInteger> resourceIds, BigInteger resourceId) {
|
||||
if (resourceId != null) {
|
||||
resourceIds.add(resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertModelReferences(Set<BigInteger> modelIds, LoginAccount account) {
|
||||
if (modelIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Model> models = modelService.listByIds(modelIds);
|
||||
boolean valid = models.size() == modelIds.size()
|
||||
&& models.stream().allMatch(model ->
|
||||
modelIds.contains(model.getId())
|
||||
&& Objects.equals(model.getTenantId(), account.getTenantId())
|
||||
&& Model.MODEL_TYPES[0].equals(model.getModelType()));
|
||||
if (!valid) {
|
||||
throw new BusinessException("模型不存在、已失效或无权使用");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertKnowledgeReferences(Set<BigInteger> knowledgeIds, LoginAccount account) {
|
||||
if (knowledgeIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<DocumentCollection> knowledges = documentCollectionService.listByIds(knowledgeIds);
|
||||
boolean valid = knowledges.size() == knowledgeIds.size()
|
||||
&& knowledges.stream().allMatch(knowledge ->
|
||||
knowledgeIds.contains(knowledge.getId())
|
||||
&& Objects.equals(knowledge.getTenantId(), account.getTenantId())
|
||||
&& resourceAccessService.canAccess(
|
||||
account, CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE));
|
||||
if (!valid) {
|
||||
throw new BusinessException("知识库不存在、已失效或无权使用");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertWorkflowReference(
|
||||
BigInteger workflowId,
|
||||
LoginAccount account,
|
||||
Set<BigInteger> checkedWorkflowIds) {
|
||||
if (workflowId == null || !checkedWorkflowIds.add(workflowId)) {
|
||||
return;
|
||||
}
|
||||
workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
workflowId,
|
||||
account,
|
||||
"子流程不存在、已禁用或无权使用");
|
||||
}
|
||||
|
||||
private void assertDatasetReference(
|
||||
JSONObject data,
|
||||
LoginAccount account,
|
||||
Set<BigInteger> checkedSourceIds,
|
||||
Set<String> checkedTableIds) {
|
||||
JSONObject datasetRef = data.getJSONObject("datasetRef");
|
||||
if (datasetRef == null) {
|
||||
return;
|
||||
}
|
||||
BigInteger sourceId = readReferenceId(datasetRef, "sourceId", "数据源");
|
||||
if (sourceId != null && checkedSourceIds.add(sourceId)) {
|
||||
requireTenantSource(sourceId, account);
|
||||
}
|
||||
BigInteger tableId = readReferenceId(datasetRef, "tableId", "数据集");
|
||||
if (tableId == null) {
|
||||
return;
|
||||
}
|
||||
if (sourceId == null) {
|
||||
throw new BusinessException("数据集缺少数据源引用");
|
||||
}
|
||||
String checkedKey = sourceId + ":" + tableId;
|
||||
if (!checkedTableIds.add(checkedKey)) {
|
||||
return;
|
||||
}
|
||||
DatacenterTable table = datacenterDatasetRegistryService.getTableWithFields(tableId);
|
||||
if (table == null
|
||||
|| !Objects.equals(table.getTenantId(), account.getTenantId())
|
||||
|| !Objects.equals(table.getSourceId(), sourceId)) {
|
||||
throw new BusinessException("数据集不存在或无权使用");
|
||||
}
|
||||
}
|
||||
|
||||
private BigInteger readReferenceId(JSONObject data, String key, String resourceName) {
|
||||
Object value = data.get(key);
|
||||
if (value == null || String.valueOf(value).isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigInteger(String.valueOf(value));
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException(resourceName + "引用格式无效");
|
||||
}
|
||||
}
|
||||
|
||||
private WorkflowDesignerOptionsView.PluginOption toPluginOption(Plugin plugin) {
|
||||
List<WorkflowDesignerOptionsView.PluginToolOption> tools =
|
||||
plugin.getTools() == null ? List.of() : plugin.getTools().stream()
|
||||
.filter(tool -> Integer.valueOf(1).equals(tool.getStatus()))
|
||||
.map(tool -> new WorkflowDesignerOptionsView.PluginToolOption(
|
||||
tool.getId(), tool.getName(), tool.getDescription()))
|
||||
.toList();
|
||||
return new WorkflowDesignerOptionsView.PluginOption(
|
||||
plugin.getId(),
|
||||
plugin.getName(),
|
||||
plugin.getDescription(),
|
||||
plugin.getIcon(),
|
||||
tools
|
||||
);
|
||||
}
|
||||
|
||||
private JSONArray parseSchemaArray(String content) {
|
||||
if (content == null || content.isBlank()) {
|
||||
return new JSONArray();
|
||||
}
|
||||
JSONArray array = JSON.parseArray(content);
|
||||
decorateSchemaArray(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
private void decorateSchemaArray(JSONArray array) {
|
||||
for (Object item : array) {
|
||||
if (!(item instanceof JSONObject value)) {
|
||||
continue;
|
||||
}
|
||||
value.put("id", cn.hutool.core.util.IdUtil.simpleUUID());
|
||||
value.put("nameDisabled", true);
|
||||
value.put("dataTypeDisabled", true);
|
||||
value.put("deleteDisabled", true);
|
||||
value.put("addChildDisabled", true);
|
||||
JSONArray children = value.getJSONArray("children");
|
||||
if (children != null) {
|
||||
decorateSchemaArray(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为子流程参数补充设计器只读元数据。
|
||||
*
|
||||
* @param array 子流程参数定义
|
||||
*/
|
||||
private void decorateChildWorkflowSchemaArray(JSONArray array) {
|
||||
for (Object item : array) {
|
||||
if (!(item instanceof JSONObject value)) {
|
||||
continue;
|
||||
}
|
||||
value.put("id", cn.hutool.core.util.IdUtil.simpleUUID());
|
||||
value.put("nameDisabled", true);
|
||||
value.put("dataTypeDisabled", true);
|
||||
value.put("deleteDisabled", true);
|
||||
value.put("addChildDisabled", true);
|
||||
value.put("refType", "ref");
|
||||
JSONArray children = value.getJSONArray("children");
|
||||
if (children != null) {
|
||||
decorateChildWorkflowSchemaArray(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSchemaHash(PluginItem record, Plugin plugin) {
|
||||
if (record.getSchemaHash() != null && !record.getSchemaHash().isBlank()) {
|
||||
return record.getSchemaHash();
|
||||
}
|
||||
if (!PluginType.isWorkflow(plugin.getType()) || plugin.getWorkflowId() == null) {
|
||||
return null;
|
||||
}
|
||||
Workflow workflow = workflowService.getPublishedById(plugin.getWorkflowId());
|
||||
return workflow == null ? null : workflowPluginSnapshotResolver.resolveSchemaHash(workflow);
|
||||
}
|
||||
|
||||
private WorkflowDesignerOptionsView.CatalogOption toCatalogOption(DatacenterCatalogMeta catalog) {
|
||||
return new WorkflowDesignerOptionsView.CatalogOption(
|
||||
catalog.getId(),
|
||||
catalog.getSourceId(),
|
||||
catalog.getCatalogName(),
|
||||
catalog.getCatalogDesc()
|
||||
);
|
||||
}
|
||||
|
||||
private WorkflowDesignerOptionsView.DatasetOption toDatasetOption(DatacenterTable table) {
|
||||
return new WorkflowDesignerOptionsView.DatasetOption(
|
||||
table.getId(),
|
||||
table.getSourceId(),
|
||||
table.getCatalogId(),
|
||||
table.getTableName(),
|
||||
table.getTableDesc()
|
||||
);
|
||||
}
|
||||
|
||||
private DatacenterSource requireTenantSource(BigInteger sourceId, LoginAccount account) {
|
||||
if (sourceId == null) {
|
||||
throw new BusinessException("数据源不能为空");
|
||||
}
|
||||
DatacenterSource source = datacenterSourceService.getById(sourceId);
|
||||
if (source == null || !Objects.equals(source.getTenantId(), account.getTenantId())) {
|
||||
throw new BusinessException("数据源不存在或无权访问");
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
private LoginAccount requireAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
package tech.easyflow.admin.service.system;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.admin.model.SystemFormOptionsView;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalFlowScopeVo;
|
||||
import tech.easyflow.approval.enums.ApprovalScopeType;
|
||||
import tech.easyflow.agent.entity.AgentCategory;
|
||||
import tech.easyflow.agent.service.AgentCategoryService;
|
||||
import tech.easyflow.ai.entity.DocumentCollectionCategory;
|
||||
import tech.easyflow.ai.entity.PluginCategory;
|
||||
import tech.easyflow.ai.entity.ResourceCategory;
|
||||
import tech.easyflow.ai.entity.WorkflowCategory;
|
||||
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
||||
import tech.easyflow.ai.service.PluginCategoryService;
|
||||
import tech.easyflow.ai.service.ResourceCategoryService;
|
||||
import tech.easyflow.ai.service.WorkflowCategoryService;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.entity.SysMenu;
|
||||
import tech.easyflow.system.entity.SysPosition;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.vo.SysRoleCategoryScopeItemVo;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
import tech.easyflow.system.service.SysMenuService;
|
||||
import tech.easyflow.system.service.SysPositionService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 查询并校验系统管理表单所需的安全选项。
|
||||
*/
|
||||
@Service
|
||||
public class SystemFormOptionService {
|
||||
|
||||
private final AgentCategoryService agentCategoryService;
|
||||
private final WorkflowCategoryService workflowCategoryService;
|
||||
private final DocumentCollectionCategoryService documentCollectionCategoryService;
|
||||
private final PluginCategoryService pluginCategoryService;
|
||||
private final ResourceCategoryService resourceCategoryService;
|
||||
private final SysDeptService sysDeptService;
|
||||
private final SysMenuService sysMenuService;
|
||||
private final SysRoleService sysRoleService;
|
||||
private final SysPositionService sysPositionService;
|
||||
|
||||
/**
|
||||
* 创建系统表单选项服务。
|
||||
*
|
||||
* @param agentCategoryService Agent 分类服务
|
||||
* @param workflowCategoryService 工作流分类服务
|
||||
* @param documentCollectionCategoryService 知识库分类服务
|
||||
* @param pluginCategoryService 插件分类服务
|
||||
* @param resourceCategoryService 素材分类服务
|
||||
* @param sysDeptService 部门服务
|
||||
* @param sysMenuService 菜单服务
|
||||
* @param sysRoleService 角色服务
|
||||
* @param sysPositionService 岗位服务
|
||||
*/
|
||||
public SystemFormOptionService(
|
||||
AgentCategoryService agentCategoryService,
|
||||
WorkflowCategoryService workflowCategoryService,
|
||||
DocumentCollectionCategoryService documentCollectionCategoryService,
|
||||
PluginCategoryService pluginCategoryService,
|
||||
ResourceCategoryService resourceCategoryService,
|
||||
SysDeptService sysDeptService,
|
||||
SysMenuService sysMenuService,
|
||||
SysRoleService sysRoleService,
|
||||
SysPositionService sysPositionService) {
|
||||
this.agentCategoryService = agentCategoryService;
|
||||
this.workflowCategoryService = workflowCategoryService;
|
||||
this.documentCollectionCategoryService = documentCollectionCategoryService;
|
||||
this.pluginCategoryService = pluginCategoryService;
|
||||
this.resourceCategoryService = resourceCategoryService;
|
||||
this.sysDeptService = sysDeptService;
|
||||
this.sysMenuService = sysMenuService;
|
||||
this.sysRoleService = sysRoleService;
|
||||
this.sysPositionService = sysPositionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询审批流程资源范围选项。
|
||||
*
|
||||
* @return 审批流程资源范围选项
|
||||
*/
|
||||
public SystemFormOptionsView.ApprovalResourceScopeOptions approvalResourceScopeOptions() {
|
||||
Map<String, List<SystemFormOptionsView.CategoryOption>> categories = new LinkedHashMap<>();
|
||||
categories.put("AGENT", listAgentCategories());
|
||||
categories.put("WORKFLOW", listWorkflowCategories());
|
||||
categories.put("KNOWLEDGE", listKnowledgeCategories());
|
||||
return new SystemFormOptionsView.ApprovalResourceScopeOptions(
|
||||
categories,
|
||||
listDepartments()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询角色管理表单选项。
|
||||
*
|
||||
* @return 角色管理表单选项
|
||||
*/
|
||||
public SystemFormOptionsView.RoleFormOptions roleFormOptions() {
|
||||
Map<String, List<SystemFormOptionsView.CategoryOption>> categories = new LinkedHashMap<>();
|
||||
categories.put("AGENT", listAgentCategories());
|
||||
categories.put("PLUGIN", listPluginCategories());
|
||||
categories.put("WORKFLOW", listWorkflowCategories());
|
||||
categories.put("KNOWLEDGE", listKnowledgeCategories());
|
||||
categories.put("RESOURCE", listResourceCategories());
|
||||
return new SystemFormOptionsView.RoleFormOptions(
|
||||
listMenus(),
|
||||
categories
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询账号管理表单选项。
|
||||
*
|
||||
* @return 账号管理表单选项
|
||||
*/
|
||||
public SystemFormOptionsView.AccountFormOptions accountFormOptions() {
|
||||
BigInteger tenantId = requireAccount().getTenantId();
|
||||
List<SystemFormOptionsView.RoleOption> roles = sysRoleService.list(QueryWrapper.create()
|
||||
.eq(SysRole::getTenantId, tenantId)
|
||||
.eq(SysRole::getStatus, EnumDataStatus.AVAILABLE.getCode())
|
||||
.orderBy(SysRole::getId, true))
|
||||
.stream()
|
||||
.map(role -> new SystemFormOptionsView.RoleOption(role.getId(), role.getRoleName()))
|
||||
.toList();
|
||||
List<SystemFormOptionsView.PositionOption> positions = sysPositionService.list(QueryWrapper.create()
|
||||
.eq(SysPosition::getTenantId, tenantId)
|
||||
.eq(SysPosition::getStatus, EnumDataStatus.AVAILABLE.getCode())
|
||||
.orderBy(SysPosition::getSortNo, true))
|
||||
.stream()
|
||||
.map(position -> new SystemFormOptionsView.PositionOption(
|
||||
position.getId(), position.getPositionName()))
|
||||
.toList();
|
||||
return new SystemFormOptionsView.AccountFormOptions(
|
||||
listDepartments(),
|
||||
roles,
|
||||
positions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验账号表单引用的部门、角色和岗位。
|
||||
*
|
||||
* @param account 账号表单
|
||||
* @throws BusinessException 引用不存在或已禁用时抛出
|
||||
*/
|
||||
public void validateAccountReferences(SysAccount account) {
|
||||
if (account == null) {
|
||||
throw new BusinessException("账号信息不能为空");
|
||||
}
|
||||
if (account.getDeptId() == null) {
|
||||
throw new BusinessException("部门不能为空");
|
||||
}
|
||||
SysDept dept = sysDeptService.getById(account.getDeptId());
|
||||
BigInteger tenantId = requireAccount().getTenantId();
|
||||
if (dept == null
|
||||
|| !Objects.equals(dept.getTenantId(), tenantId)
|
||||
|| !EnumDataStatus.AVAILABLE.getCode().equals(dept.getStatus())) {
|
||||
throw new BusinessException("部门不存在或已禁用");
|
||||
}
|
||||
assertAvailableIds(
|
||||
account.getRoleIds(),
|
||||
sysRoleService::listByIds,
|
||||
SysRole::getId,
|
||||
SysRole::getStatus,
|
||||
SysRole::getTenantId,
|
||||
tenantId,
|
||||
"角色"
|
||||
);
|
||||
if (account.getPositionIds() != null && !account.getPositionIds().isEmpty()) {
|
||||
assertAvailableIds(
|
||||
account.getPositionIds(),
|
||||
sysPositionService::listByIds,
|
||||
SysPosition::getId,
|
||||
SysPosition::getStatus,
|
||||
SysPosition::getTenantId,
|
||||
tenantId,
|
||||
"岗位"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验角色引用的菜单和部门。
|
||||
*
|
||||
* @param role 角色表单
|
||||
* @throws BusinessException 引用不存在时抛出
|
||||
*/
|
||||
public void validateRoleReferences(SysRole role) {
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色信息不能为空");
|
||||
}
|
||||
assertExistingIds(role.getMenuIds(), sysMenuService::listByIds, SysMenu::getId, "菜单");
|
||||
if (role.getDeptIds() != null && !role.getDeptIds().isEmpty()) {
|
||||
assertTenantIds(
|
||||
role.getDeptIds(),
|
||||
sysDeptService::listByIds,
|
||||
SysDept::getId,
|
||||
SysDept::getTenantId,
|
||||
requireAccount().getTenantId(),
|
||||
"部门");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验角色分类授权中提交的非 Bot 分类 ID。
|
||||
*
|
||||
* @param scopes 分类权限范围
|
||||
* @throws BusinessException 分类类型或分类 ID 非法时抛出
|
||||
*/
|
||||
public void validateCategoryScopes(List<SysRoleCategoryScopeItemVo> scopes) {
|
||||
if (scopes == null) {
|
||||
return;
|
||||
}
|
||||
for (SysRoleCategoryScopeItemVo scope : scopes) {
|
||||
if (scope == null || scope.getCategoryIds() == null || scope.getCategoryIds().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
switch (String.valueOf(scope.getResourceType()).toUpperCase()) {
|
||||
case "AGENT" -> assertTenantIds(
|
||||
scope.getCategoryIds(),
|
||||
agentCategoryService::listByIds,
|
||||
AgentCategory::getId,
|
||||
AgentCategory::getTenantId,
|
||||
requireAccount().getTenantId(),
|
||||
"Agent 分类");
|
||||
case "PLUGIN" -> assertExistingIds(
|
||||
scope.getCategoryIds(), pluginCategoryService::listByIds, PluginCategory::getId, "插件分类");
|
||||
case "WORKFLOW" -> assertExistingIds(
|
||||
scope.getCategoryIds(), workflowCategoryService::listByIds, WorkflowCategory::getId, "工作流分类");
|
||||
case "KNOWLEDGE" -> assertExistingIds(
|
||||
scope.getCategoryIds(),
|
||||
documentCollectionCategoryService::listByIds,
|
||||
DocumentCollectionCategory::getId,
|
||||
"知识库分类");
|
||||
case "RESOURCE" -> assertExistingIds(
|
||||
scope.getCategoryIds(), resourceCategoryService::listByIds, ResourceCategory::getId, "素材分类");
|
||||
case "BOT" -> throw new BusinessException("Bot 分类授权已停止维护");
|
||||
default -> throw new BusinessException("不支持的分类资源类型");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验审批流程提交的分类和部门范围。
|
||||
*
|
||||
* @param resourceType 审批资源类型
|
||||
* @param scopes 审批范围
|
||||
* @throws BusinessException 范围引用不存在或资源类型不匹配时抛出
|
||||
*/
|
||||
public void validateApprovalScopes(String resourceType, List<ApprovalFlowScopeVo> scopes) {
|
||||
if (scopes == null) {
|
||||
return;
|
||||
}
|
||||
for (ApprovalFlowScopeVo scope : scopes) {
|
||||
if (scope == null || scope.getScopeValue() == null) {
|
||||
continue;
|
||||
}
|
||||
String scopeType = String.valueOf(scope.getScopeType()).toUpperCase();
|
||||
if (ApprovalScopeType.DEPT.getCode().equals(scopeType)) {
|
||||
assertTenantIds(
|
||||
List.of(scope.getScopeValue()),
|
||||
sysDeptService::listByIds,
|
||||
SysDept::getId,
|
||||
SysDept::getTenantId,
|
||||
requireAccount().getTenantId(),
|
||||
"部门");
|
||||
continue;
|
||||
}
|
||||
if (!ApprovalScopeType.CATEGORY.getCode().equals(scopeType)) {
|
||||
throw new BusinessException("不支持的审批范围类型");
|
||||
}
|
||||
switch (String.valueOf(resourceType).toUpperCase()) {
|
||||
case "AGENT" -> assertTenantIds(
|
||||
List.of(scope.getScopeValue()),
|
||||
agentCategoryService::listByIds,
|
||||
AgentCategory::getId,
|
||||
AgentCategory::getTenantId,
|
||||
requireAccount().getTenantId(),
|
||||
"Agent 分类");
|
||||
case "WORKFLOW" -> assertExistingIds(
|
||||
List.of(scope.getScopeValue()),
|
||||
workflowCategoryService::listByIds,
|
||||
WorkflowCategory::getId,
|
||||
"工作流分类");
|
||||
case "KNOWLEDGE" -> assertExistingIds(
|
||||
List.of(scope.getScopeValue()),
|
||||
documentCollectionCategoryService::listByIds,
|
||||
DocumentCollectionCategory::getId,
|
||||
"知识库分类");
|
||||
default -> throw new BusinessException("当前资源类型不支持分类审批范围");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.DepartmentOption> listDepartments() {
|
||||
BigInteger tenantId = requireAccount().getTenantId();
|
||||
List<SysDept> departments = sysDeptService.list(QueryWrapper.create()
|
||||
.eq(SysDept::getTenantId, tenantId)
|
||||
.eq(SysDept::getStatus, EnumDataStatus.AVAILABLE.getCode())
|
||||
.orderBy(SysDept::getSortNo, true));
|
||||
return buildDepartmentTree(departments);
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.MenuOption> listMenus() {
|
||||
List<SysMenu> menus = sysMenuService.list(QueryWrapper.create()
|
||||
.orderBy(SysMenu::getSortNo, true));
|
||||
return buildMenuTree(menus);
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.CategoryOption> listAgentCategories() {
|
||||
BigInteger tenantId = requireAccount().getTenantId();
|
||||
return agentCategoryService.list(QueryWrapper.create()
|
||||
.eq(AgentCategory::getTenantId, tenantId)
|
||||
.eq(AgentCategory::getStatus, EnumDataStatus.AVAILABLE.getCode())
|
||||
.orderBy(AgentCategory::getSortNo, true))
|
||||
.stream()
|
||||
.map(category -> new SystemFormOptionsView.CategoryOption(
|
||||
category.getId(), category.getCategoryName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.CategoryOption> listWorkflowCategories() {
|
||||
return workflowCategoryService.list(QueryWrapper.create()
|
||||
.orderBy(WorkflowCategory::getSortNo, true))
|
||||
.stream()
|
||||
.map(category -> new SystemFormOptionsView.CategoryOption(
|
||||
category.getId(), category.getCategoryName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.CategoryOption> listKnowledgeCategories() {
|
||||
return documentCollectionCategoryService.list(QueryWrapper.create()
|
||||
.orderBy(DocumentCollectionCategory::getSortNo, true))
|
||||
.stream()
|
||||
.map(category -> new SystemFormOptionsView.CategoryOption(
|
||||
category.getId(), category.getCategoryName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.CategoryOption> listPluginCategories() {
|
||||
return pluginCategoryService.list(QueryWrapper.create()
|
||||
.orderBy(PluginCategory::getId, true))
|
||||
.stream()
|
||||
.map(category -> new SystemFormOptionsView.CategoryOption(
|
||||
category.getId(), category.getName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.CategoryOption> listResourceCategories() {
|
||||
return resourceCategoryService.list(QueryWrapper.create()
|
||||
.orderBy(ResourceCategory::getSortNo, true))
|
||||
.stream()
|
||||
.map(category -> new SystemFormOptionsView.CategoryOption(
|
||||
category.getId(), category.getCategoryName()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.DepartmentOption> buildDepartmentTree(List<SysDept> departments) {
|
||||
Set<BigInteger> ids = new HashSet<>();
|
||||
departments.forEach(item -> ids.add(item.getId()));
|
||||
Map<BigInteger, List<SysDept>> children = new LinkedHashMap<>();
|
||||
List<SysDept> roots = new ArrayList<>();
|
||||
for (SysDept department : departments) {
|
||||
BigInteger parentId = department.getParentId();
|
||||
if (parentId == null || BigInteger.ZERO.equals(parentId) || !ids.contains(parentId)) {
|
||||
roots.add(department);
|
||||
} else {
|
||||
children.computeIfAbsent(parentId, ignored -> new ArrayList<>()).add(department);
|
||||
}
|
||||
}
|
||||
return roots.stream().map(item -> toDepartmentOption(item, children)).toList();
|
||||
}
|
||||
|
||||
private SystemFormOptionsView.DepartmentOption toDepartmentOption(
|
||||
SysDept department,
|
||||
Map<BigInteger, List<SysDept>> children) {
|
||||
return new SystemFormOptionsView.DepartmentOption(
|
||||
department.getId(),
|
||||
department.getParentId(),
|
||||
department.getDeptName(),
|
||||
children.getOrDefault(department.getId(), List.of()).stream()
|
||||
.map(item -> toDepartmentOption(item, children))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
private List<SystemFormOptionsView.MenuOption> buildMenuTree(List<SysMenu> menus) {
|
||||
Set<BigInteger> ids = new HashSet<>();
|
||||
menus.forEach(item -> ids.add(item.getId()));
|
||||
Map<BigInteger, List<SysMenu>> children = new LinkedHashMap<>();
|
||||
List<SysMenu> roots = new ArrayList<>();
|
||||
for (SysMenu menu : menus) {
|
||||
BigInteger parentId = menu.getParentId();
|
||||
if (parentId == null || BigInteger.ZERO.equals(parentId) || !ids.contains(parentId)) {
|
||||
roots.add(menu);
|
||||
} else {
|
||||
children.computeIfAbsent(parentId, ignored -> new ArrayList<>()).add(menu);
|
||||
}
|
||||
}
|
||||
return roots.stream().map(item -> toMenuOption(item, children)).toList();
|
||||
}
|
||||
|
||||
private SystemFormOptionsView.MenuOption toMenuOption(
|
||||
SysMenu menu,
|
||||
Map<BigInteger, List<SysMenu>> children) {
|
||||
return new SystemFormOptionsView.MenuOption(
|
||||
menu.getId(),
|
||||
menu.getParentId(),
|
||||
menu.getMenuTitle(),
|
||||
children.getOrDefault(menu.getId(), List.of()).stream()
|
||||
.map(item -> toMenuOption(item, children))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
private <T> void assertAvailableIds(
|
||||
Collection<BigInteger> rawIds,
|
||||
Function<Collection<BigInteger>, List<T>> loader,
|
||||
Function<T, BigInteger> idGetter,
|
||||
Function<T, Integer> statusGetter,
|
||||
Function<T, BigInteger> tenantGetter,
|
||||
BigInteger tenantId,
|
||||
String label) {
|
||||
Set<BigInteger> ids = normalizeIds(rawIds);
|
||||
if (ids.isEmpty()) {
|
||||
throw new BusinessException(label + "不能为空");
|
||||
}
|
||||
List<T> records = loader.apply(ids);
|
||||
boolean valid = records.size() == ids.size()
|
||||
&& records.stream().allMatch(item ->
|
||||
ids.contains(idGetter.apply(item))
|
||||
&& EnumDataStatus.AVAILABLE.getCode().equals(statusGetter.apply(item))
|
||||
&& Objects.equals(tenantGetter.apply(item), tenantId));
|
||||
if (!valid) {
|
||||
throw new BusinessException(label + "不存在或已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void assertExistingIds(
|
||||
Collection<BigInteger> rawIds,
|
||||
Function<Collection<BigInteger>, List<T>> loader,
|
||||
Function<T, BigInteger> idGetter,
|
||||
String label) {
|
||||
Set<BigInteger> ids = normalizeIds(rawIds);
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<T> records = loader.apply(ids);
|
||||
boolean valid = records.size() == ids.size()
|
||||
&& records.stream().allMatch(item -> ids.contains(idGetter.apply(item)));
|
||||
if (!valid) {
|
||||
throw new BusinessException(label + "不存在或无权访问");
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void assertTenantIds(
|
||||
Collection<BigInteger> rawIds,
|
||||
Function<Collection<BigInteger>, List<T>> loader,
|
||||
Function<T, BigInteger> idGetter,
|
||||
Function<T, BigInteger> tenantGetter,
|
||||
BigInteger tenantId,
|
||||
String label) {
|
||||
Set<BigInteger> ids = normalizeIds(rawIds);
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<T> records = loader.apply(ids);
|
||||
boolean valid = records.size() == ids.size()
|
||||
&& records.stream().allMatch(item ->
|
||||
ids.contains(idGetter.apply(item))
|
||||
&& Objects.equals(tenantGetter.apply(item), tenantId));
|
||||
if (!valid) {
|
||||
throw new BusinessException(label + "不存在或无权访问");
|
||||
}
|
||||
}
|
||||
|
||||
private LoginAccount requireAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getTenantId() == null) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
private Set<BigInteger> normalizeIds(Collection<BigInteger> rawIds) {
|
||||
Set<BigInteger> ids = new LinkedHashSet<>();
|
||||
if (rawIds != null) {
|
||||
rawIds.stream().filter(Objects::nonNull).forEach(ids::add);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package tech.easyflow.admin.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.admin.controller.agent.AgentSessionController;
|
||||
import tech.easyflow.admin.controller.ai.ChatHistoryController;
|
||||
import tech.easyflow.admin.controller.ai.ModelController;
|
||||
import tech.easyflow.admin.controller.ai.WorkFlowNodeController;
|
||||
import tech.easyflow.admin.controller.ai.WorkflowController;
|
||||
import tech.easyflow.admin.controller.dashboard.DashboardController;
|
||||
import tech.easyflow.admin.controller.job.SysJobController;
|
||||
import tech.easyflow.admin.controller.system.ApprovalFlowController;
|
||||
import tech.easyflow.admin.controller.system.SysAccountController;
|
||||
import tech.easyflow.admin.controller.system.SysRoleController;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 管理端页面能力接口权限归属契约测试。
|
||||
*/
|
||||
public class PermissionIsolationContractTest {
|
||||
|
||||
/**
|
||||
* 验证工作流设计器依赖的选项接口只要求工作流查询权限。
|
||||
*/
|
||||
@Test
|
||||
public void workflowDesignerOptionsBelongToWorkflowPermission() {
|
||||
assertMethodPermission(
|
||||
WorkflowController.class,
|
||||
"designerOptions",
|
||||
"/api/v1/workflow/query"
|
||||
);
|
||||
assertMethodPermission(
|
||||
WorkflowController.class,
|
||||
"designerChildWorkflow",
|
||||
"/api/v1/workflow/query"
|
||||
);
|
||||
assertMethodPermission(
|
||||
WorkFlowNodeController.class,
|
||||
"getChainParams",
|
||||
"/api/v1/workflow/query"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证各管理页面的辅助能力接口使用页面自身权限。
|
||||
*/
|
||||
@Test
|
||||
public void pageOptionsBelongToOwningPagePermissions() {
|
||||
assertMethodPermission(ModelController.class, "gatewayConfig", "/api/v1/model/query");
|
||||
assertMethodPermission(DashboardController.class, "agentOptions", "/api/v1/dashboard/query");
|
||||
assertMethodPermission(SysJobController.class, "workflowOptions", "/api/v1/sysJob/save");
|
||||
assertMethodPermission(SysJobController.class, "getNextTimes", "/api/v1/sysJob/save");
|
||||
assertMethodPermission(ApprovalFlowController.class, "resourceScopeOptions", "/api/v1/approvalFlow/save");
|
||||
assertMethodPermission(SysRoleController.class, "formOptions", "/api/v1/sysRole/query");
|
||||
assertMethodPermission(SysAccountController.class, "formOptions", "/api/v1/sysAccount/save");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Agent 会话和聊天历史接口分别使用各自页面权限。
|
||||
*/
|
||||
@Test
|
||||
public void agentSessionAndHistoryUseIndependentPermissions() {
|
||||
assertClassPermission(AgentSessionController.class, "/api/v1/agent/session/query");
|
||||
assertClassPermission(ChatHistoryController.class, "/api/v1/chatHistory/query");
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言控制器方法只声明指定权限。
|
||||
*
|
||||
* @param controllerType 控制器类型
|
||||
* @param methodName 方法名
|
||||
* @param expectedPermission 期望权限
|
||||
*/
|
||||
private void assertMethodPermission(
|
||||
Class<?> controllerType,
|
||||
String methodName,
|
||||
String expectedPermission) {
|
||||
Method method = Arrays.stream(controllerType.getDeclaredMethods())
|
||||
.filter(candidate -> methodName.equals(candidate.getName()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AssertionError("未找到控制器方法:" + methodName));
|
||||
SaCheckPermission permission = method.getAnnotation(SaCheckPermission.class);
|
||||
|
||||
Assert.assertNotNull(permission, methodName + " 缺少权限注解");
|
||||
Assert.assertEquals(permission.value(), new String[]{expectedPermission});
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言控制器类只声明指定权限。
|
||||
*
|
||||
* @param controllerType 控制器类型
|
||||
* @param expectedPermission 期望权限
|
||||
*/
|
||||
private void assertClassPermission(Class<?> controllerType, String expectedPermission) {
|
||||
SaCheckPermission permission = controllerType.getAnnotation(SaCheckPermission.class);
|
||||
|
||||
Assert.assertNotNull(permission, controllerType.getSimpleName() + " 缺少权限注解");
|
||||
Assert.assertEquals(permission.value(), new String[]{expectedPermission});
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
|
||||
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
||||
import tech.easyflow.agent.service.AgentOptionQueryService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
@@ -30,7 +31,8 @@ public class ChatHistoryControllerTest {
|
||||
BigInteger accountId = BigInteger.valueOf(20);
|
||||
ChatHistoryManageService service = mock(ChatHistoryManageService.class);
|
||||
CategoryPermissionService permissionService = mock(CategoryPermissionService.class);
|
||||
ChatHistoryController controller = new ChatHistoryController(service, permissionService);
|
||||
ChatHistoryController controller = new ChatHistoryController(
|
||||
service, permissionService, mock(AgentOptionQueryService.class));
|
||||
ChatSessionFilterQuery query = new ChatSessionFilterQuery();
|
||||
LoginAccount account = loginAccount(accountId);
|
||||
when(permissionService.isSuperAdmin(account)).thenReturn(false);
|
||||
@@ -54,7 +56,8 @@ public class ChatHistoryControllerTest {
|
||||
BigInteger sessionId = BigInteger.valueOf(30);
|
||||
ChatHistoryManageService service = mock(ChatHistoryManageService.class);
|
||||
CategoryPermissionService permissionService = mock(CategoryPermissionService.class);
|
||||
ChatHistoryController controller = new ChatHistoryController(service, permissionService);
|
||||
ChatHistoryController controller = new ChatHistoryController(
|
||||
service, permissionService, mock(AgentOptionQueryService.class));
|
||||
LoginAccount account = loginAccount(accountId);
|
||||
when(permissionService.isSuperAdmin(account)).thenReturn(true);
|
||||
when(service.getAdminSession(accountId, true, sessionId)).thenReturn(new ChatSessionSummary());
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
@@ -22,6 +23,7 @@ import java.util.Map;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -37,13 +39,19 @@ public class SysJobControllerTest {
|
||||
BigInteger workflowId = BigInteger.valueOf(101);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
WorkflowUsageAuthorizationService workflowAuthorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
WorkflowRunningParameterResolver parameterResolver =
|
||||
mock(WorkflowRunningParameterResolver.class);
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(workflowId);
|
||||
workflow.setContent("{}");
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
when(workflowAuthorizationService.requireUsableWorkflow(
|
||||
org.mockito.ArgumentMatchers.eq(workflowId),
|
||||
org.mockito.ArgumentMatchers.any(LoginAccount.class),
|
||||
org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(workflow);
|
||||
Parameter requiredParameter = mock(Parameter.class);
|
||||
when(requiredParameter.isRequired()).thenReturn(true);
|
||||
when(requiredParameter.getName()).thenReturn("user_input");
|
||||
@@ -53,6 +61,7 @@ public class SysJobControllerTest {
|
||||
SysJobController controller = new SysJobController(
|
||||
jobService,
|
||||
workflowService,
|
||||
workflowAuthorizationService,
|
||||
resourceAccessService,
|
||||
parameterResolver
|
||||
);
|
||||
@@ -76,4 +85,66 @@ public class SysJobControllerTest {
|
||||
Assert.assertTrue(exception.getMessage().contains("用户问题"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证部分更新省略任务类型时仍按数据库中的工作流任务类型完成引用校验。
|
||||
*/
|
||||
@Test
|
||||
public void shouldValidateMergedWorkflowReferenceOnPartialUpdate() {
|
||||
BigInteger jobId = BigInteger.valueOf(201);
|
||||
BigInteger oldWorkflowId = BigInteger.valueOf(301);
|
||||
BigInteger newWorkflowId = BigInteger.valueOf(302);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
WorkflowUsageAuthorizationService workflowAuthorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
WorkflowRunningParameterResolver parameterResolver =
|
||||
mock(WorkflowRunningParameterResolver.class);
|
||||
|
||||
SysJob existing = new SysJob();
|
||||
existing.setId(jobId);
|
||||
existing.setJobType(EnumJobType.TINY_FLOW.getCode());
|
||||
existing.setJobParams(Map.of(
|
||||
JobConstant.WORKFLOW_KEY, oldWorkflowId.toString(),
|
||||
JobConstant.WORKFLOW_PARAMS_KEY, Map.of()
|
||||
));
|
||||
when(jobService.getById(jobId)).thenReturn(existing);
|
||||
when(workflowAuthorizationService.requireUsableWorkflow(
|
||||
org.mockito.ArgumentMatchers.eq(newWorkflowId),
|
||||
org.mockito.ArgumentMatchers.any(LoginAccount.class),
|
||||
org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenThrow(new BusinessException("无权限运行所选工作流"));
|
||||
|
||||
SysJob update = new SysJob();
|
||||
update.setId(jobId);
|
||||
update.setJobParams(Map.of(
|
||||
JobConstant.WORKFLOW_KEY, newWorkflowId.toString(),
|
||||
JobConstant.WORKFLOW_PARAMS_KEY, Map.of()
|
||||
));
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.ONE);
|
||||
|
||||
SysJobController controller = new SysJobController(
|
||||
jobService,
|
||||
workflowService,
|
||||
workflowAuthorizationService,
|
||||
resourceAccessService,
|
||||
parameterResolver
|
||||
);
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
|
||||
BusinessException exception = Assert.expectThrows(
|
||||
BusinessException.class,
|
||||
() -> controller.onSaveOrUpdateBefore(update, false)
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("无权限"));
|
||||
verify(workflowAuthorizationService).requireUsableWorkflow(
|
||||
org.mockito.ArgumentMatchers.eq(newWorkflowId),
|
||||
org.mockito.ArgumentMatchers.eq(account),
|
||||
org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo;
|
||||
import tech.easyflow.admin.service.system.SystemFormOptionService;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
@@ -46,7 +47,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
mock(SysAccountService.class),
|
||||
mock(AuthCredentialKeyService.class),
|
||||
mock(SysRoleService.class)
|
||||
mock(SysRoleService.class),
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getParameter("keyword")).thenReturn(" search-user ");
|
||||
@@ -74,7 +76,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
mock(SysAccountService.class),
|
||||
mock(AuthCredentialKeyService.class),
|
||||
mock(SysRoleService.class)
|
||||
mock(SysRoleService.class),
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getParameter("keyword")).thenReturn(" ");
|
||||
@@ -93,7 +96,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
mock(SysAccountService.class),
|
||||
mock(AuthCredentialKeyService.class),
|
||||
mock(SysRoleService.class)
|
||||
mock(SysRoleService.class),
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getParameter("keyword")).thenReturn("a_b%c\\d");
|
||||
@@ -118,7 +122,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
roleService,
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
SysAccount entity = createAccount(selectedDeptId);
|
||||
LoginAccount loginAccount = createLoginAccount(operatorId, tenantId, operatorDeptId);
|
||||
@@ -167,7 +172,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
roleService,
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
SysAccount entity = createAccount(BigInteger.valueOf(200));
|
||||
entity.setRoleIds(List.of());
|
||||
@@ -202,7 +208,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
roleService,
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
@@ -233,7 +240,8 @@ public class SysAccountControllerTest {
|
||||
SysAccountController controller = new SysAccountController(
|
||||
accountService,
|
||||
credentialKeyService,
|
||||
roleService
|
||||
roleService,
|
||||
mock(SystemFormOptionService.class)
|
||||
);
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package tech.easyflow.admin.service.ai;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.PluginService;
|
||||
import tech.easyflow.ai.service.PluginVisibilityService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
|
||||
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterSourceService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link WorkflowDesignerOptionService} 工作流引用权限测试。
|
||||
*/
|
||||
public class WorkflowDesignerOptionServiceTest {
|
||||
|
||||
/**
|
||||
* 验证客户端提交候选列表之外的模型 ID 时服务端拒绝保存。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectModelOutsideSelectableOptions() {
|
||||
ModelService modelService = mock(ModelService.class);
|
||||
DocumentCollectionService knowledgeService = mock(DocumentCollectionService.class);
|
||||
when(modelService.listByIds(any())).thenReturn(List.of());
|
||||
when(knowledgeService.list(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
WorkflowDesignerOptionService service = createService(
|
||||
modelService, knowledgeService, mock(DatacenterSourceService.class));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount());
|
||||
|
||||
BusinessException exception = Assert.expectThrows(
|
||||
BusinessException.class,
|
||||
() -> service.assertContentReferences(
|
||||
"{\"nodes\":[{\"type\":\"llmNode\",\"data\":{\"llmId\":\"99\"}}]}")
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("模型"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工作流数据节点不能引用其他租户的数据源。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectCrossTenantDataSource() {
|
||||
ModelService modelService = mock(ModelService.class);
|
||||
DocumentCollectionService knowledgeService = mock(DocumentCollectionService.class);
|
||||
DatacenterSourceService sourceService = mock(DatacenterSourceService.class);
|
||||
when(modelService.listSelectableModels(any(Model.class), eq(false), eq("id"), eq("desc")))
|
||||
.thenReturn(List.of());
|
||||
when(knowledgeService.list(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
DatacenterSource source = new DatacenterSource();
|
||||
source.setId(BigInteger.valueOf(9));
|
||||
source.setTenantId(BigInteger.valueOf(200));
|
||||
when(sourceService.getById(BigInteger.valueOf(9))).thenReturn(source);
|
||||
WorkflowDesignerOptionService service = createService(
|
||||
modelService, knowledgeService, sourceService);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount());
|
||||
|
||||
BusinessException exception = Assert.expectThrows(
|
||||
BusinessException.class,
|
||||
() -> service.assertContentReferences("""
|
||||
{"nodes":[{"type":"search-dataset-node","data":{
|
||||
"datasetRef":{"sourceId":"9"}
|
||||
}}]}
|
||||
""")
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("数据源"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证子流程节点配置拒绝读取其他租户的工作流。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectCrossTenantChildWorkflow() {
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(BigInteger.valueOf(19));
|
||||
workflow.setTenantId(BigInteger.valueOf(200));
|
||||
when(workflowService.getById(BigInteger.valueOf(19))).thenReturn(workflow);
|
||||
WorkflowDesignerOptionService service = createService(
|
||||
mock(ModelService.class),
|
||||
mock(DocumentCollectionService.class),
|
||||
mock(DatacenterSourceService.class),
|
||||
workflowService
|
||||
);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount());
|
||||
|
||||
BusinessException exception = Assert.expectThrows(
|
||||
BusinessException.class,
|
||||
() -> service.getChildWorkflowNodeData(
|
||||
BigInteger.valueOf(10),
|
||||
BigInteger.valueOf(19))
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("子流程"));
|
||||
}
|
||||
}
|
||||
|
||||
private WorkflowDesignerOptionService createService(
|
||||
ModelService modelService,
|
||||
DocumentCollectionService knowledgeService,
|
||||
DatacenterSourceService sourceService) {
|
||||
return createService(modelService, knowledgeService, sourceService, mock(WorkflowService.class));
|
||||
}
|
||||
|
||||
private WorkflowDesignerOptionService createService(
|
||||
ModelService modelService,
|
||||
DocumentCollectionService knowledgeService,
|
||||
DatacenterSourceService sourceService,
|
||||
WorkflowService workflowService) {
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
return new WorkflowDesignerOptionService(
|
||||
modelService,
|
||||
knowledgeService,
|
||||
mock(PluginService.class),
|
||||
mock(PluginItemService.class),
|
||||
mock(PluginVisibilityService.class),
|
||||
workflowService,
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService),
|
||||
mock(WorkflowPluginSnapshotResolver.class),
|
||||
mock(ChainParser.class),
|
||||
mock(WorkflowDatacenterContentService.class),
|
||||
resourceAccessService,
|
||||
sourceService,
|
||||
mock(DatacenterDatasetRegistryService.class),
|
||||
mock(DatacenterDatasetQueryService.class)
|
||||
);
|
||||
}
|
||||
|
||||
private LoginAccount loginAccount() {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.ONE);
|
||||
account.setTenantId(BigInteger.valueOf(100));
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -3,36 +3,31 @@ package tech.easyflow.ai.config;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import tech.easyflow.ai.mapper.*;
|
||||
import tech.easyflow.ai.mapper.BotCategoryMapper;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.dict.DictManager;
|
||||
import tech.easyflow.common.dict.loader.DbDataLoader;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 注册仍由 Bot 兼容页面使用的数据库字典。
|
||||
*/
|
||||
@Configuration
|
||||
public class AiDictAutoConfig {
|
||||
|
||||
@Resource
|
||||
private WorkflowMapper workflowMapper;
|
||||
@Resource
|
||||
private WorkflowCategoryMapper workflowCategoryMapper;
|
||||
/** Bot 分类字典数据访问器。 */
|
||||
@Resource
|
||||
private BotCategoryMapper botCategoryMapper;
|
||||
@Resource
|
||||
private ResourceCategoryMapper resourceCategoryMapper;
|
||||
@Resource
|
||||
private DocumentCollectionCategoryMapper documentCollectionCategoryMapper;
|
||||
|
||||
/**
|
||||
* 应用启动完成后注册 Bot 兼容字典。
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationStartup() {
|
||||
|
||||
DictManager dictManager = SpringContextUtil.getBean(DictManager.class);
|
||||
dictManager.putLoader(new DbDataLoader<>("aiWorkFlow", workflowMapper, "id", "title", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiWorkFlowCategory", workflowCategoryMapper, "id", "category_name", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiBotCategory", botCategoryMapper, "id", "category_name", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiResourceCategory", resourceCategoryMapper, "id", "category_name", null, null, false));
|
||||
dictManager.putLoader(new DbDataLoader<>("aiDocumentCollectionCategory", documentCollectionCategoryMapper, "id", "category_name", null, null, false));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 工作流使用权限校验服务。
|
||||
*
|
||||
* <p>统一封装工作流存在性、租户、启用状态和资源使用权限校验,供页面能力和后台任务复用。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowUsageAuthorizationService {
|
||||
|
||||
/** 工作流服务。 */
|
||||
private final WorkflowService workflowService;
|
||||
|
||||
/** 资源访问控制服务。 */
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建工作流使用权限校验服务。
|
||||
*
|
||||
* @param workflowService 工作流服务
|
||||
* @param resourceAccessService 资源访问控制服务
|
||||
*/
|
||||
public WorkflowUsageAuthorizationService(
|
||||
WorkflowService workflowService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
this.workflowService = workflowService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前账号可使用的启用工作流。
|
||||
*
|
||||
* @param workflowId 工作流 ID
|
||||
* @param account 使用工作流的账号
|
||||
* @param denyMessage 校验失败提示
|
||||
* @return 可使用的工作流
|
||||
* @throws BusinessException 工作流不存在、未启用、跨租户或无使用权限时抛出
|
||||
*/
|
||||
public Workflow requireUsableWorkflow(
|
||||
BigInteger workflowId,
|
||||
LoginAccount account,
|
||||
String denyMessage) {
|
||||
String message = denyMessage == null || denyMessage.isBlank()
|
||||
? "工作流不存在、已禁用或无权使用"
|
||||
: denyMessage;
|
||||
if (workflowId == null || account == null || account.getId() == null
|
||||
|| account.getTenantId() == null) {
|
||||
throw new BusinessException(403, 403, message);
|
||||
}
|
||||
Workflow workflow = workflowService.getById(workflowId);
|
||||
boolean usable = workflow != null
|
||||
&& Objects.equals(workflow.getTenantId(), account.getTenantId())
|
||||
&& EnumDataStatus.AVAILABLE.getCode().equals(workflow.getStatus())
|
||||
&& resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE);
|
||||
if (!usable) {
|
||||
throw new BusinessException(403, 403, message);
|
||||
}
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,14 @@ public final class WorkflowSharePolicy {
|
||||
private static final Set<String> ALLOWED_REQUESTS = Set.of(
|
||||
permissionKey("GET", "/api/v1/workflow/detail", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/getRunningParameters", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/options", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/plugins", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/pluginTinyFlow", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/childWorkflow", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/dataSources", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/catalogs", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/managedTables", ResourceAction.READ),
|
||||
permissionKey("GET", "/api/v1/workflow/designer/schema", ResourceAction.READ),
|
||||
permissionKey("POST", "/api/v1/workflow/update", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/check", ResourceAction.MANAGE),
|
||||
permissionKey("POST", "/api/v1/workflow/singleRun", ResourceAction.USE),
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link WorkflowUsageAuthorizationService} 工作流使用权限校验测试。
|
||||
*/
|
||||
public class WorkflowUsageAuthorizationServiceTest {
|
||||
|
||||
/**
|
||||
* 验证禁用工作流即使资源权限允许也不能被使用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectDisabledWorkflow() {
|
||||
BigInteger workflowId = BigInteger.valueOf(101);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
Workflow workflow = workflow(workflowId, BigInteger.TEN, EnumDataStatus.UNAVAILABLE.getCode());
|
||||
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
when(resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE)).thenReturn(true);
|
||||
WorkflowUsageAuthorizationService service =
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
|
||||
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.requireUsableWorkflow(workflowId, account, "工作流不可用")
|
||||
);
|
||||
|
||||
Assert.assertEquals(exception.getMessage(), "工作流不可用");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工作流与账号租户不一致时拒绝使用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectCrossTenantWorkflow() {
|
||||
BigInteger workflowId = BigInteger.valueOf(102);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
Workflow workflow = workflow(
|
||||
workflowId,
|
||||
BigInteger.valueOf(20),
|
||||
EnumDataStatus.AVAILABLE.getCode());
|
||||
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
WorkflowUsageAuthorizationService service =
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
|
||||
|
||||
Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.requireUsableWorkflow(workflowId, account, "工作流不可用")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证启用、同租户且具有使用权限的工作流可以返回。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnUsableWorkflow() {
|
||||
BigInteger workflowId = BigInteger.valueOf(103);
|
||||
WorkflowService workflowService = mock(WorkflowService.class);
|
||||
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||
Workflow workflow = workflow(
|
||||
workflowId,
|
||||
BigInteger.TEN,
|
||||
EnumDataStatus.AVAILABLE.getCode());
|
||||
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
|
||||
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||
when(resourceAccessService.canAccess(
|
||||
account,
|
||||
CategoryResourceType.WORKFLOW,
|
||||
workflow,
|
||||
ResourceAction.USE)).thenReturn(true);
|
||||
WorkflowUsageAuthorizationService service =
|
||||
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
|
||||
|
||||
Workflow result = service.requireUsableWorkflow(workflowId, account, "工作流不可用");
|
||||
|
||||
Assert.assertSame(result, workflow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作流测试数据。
|
||||
*
|
||||
* @param id 工作流 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param status 工作流状态
|
||||
* @return 工作流
|
||||
*/
|
||||
private Workflow workflow(BigInteger id, BigInteger tenantId, Integer status) {
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(id);
|
||||
workflow.setTenantId(tenantId);
|
||||
workflow.setStatus(status);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建登录账号测试数据。
|
||||
*
|
||||
* @param id 账号 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @return 登录账号
|
||||
*/
|
||||
private LoginAccount account(BigInteger id, BigInteger tenantId) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(id);
|
||||
account.setTenantId(tenantId);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,11 @@ public class WorkflowSharePolicyTest {
|
||||
"/api/v1/workflow/detail",
|
||||
ResourceAction.READ
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"GET",
|
||||
"/api/v1/workflow/designer/childWorkflow",
|
||||
ResourceAction.READ
|
||||
));
|
||||
Assert.assertTrue(WorkflowSharePolicy.isAllowedRequest(
|
||||
"POST",
|
||||
"/api/v1/workflow/update",
|
||||
|
||||
@@ -42,5 +42,17 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.12.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package tech.easyflow.job.service;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 工作流定时任务执行服务。
|
||||
*
|
||||
* <p>每次触发都重新加载任务、账号和工作流,并按服务端记录恢复执行主体及重新授权。</p>
|
||||
*/
|
||||
@Service
|
||||
public class WorkflowJobExecutionService {
|
||||
|
||||
/** 定时任务服务。 */
|
||||
private final SysJobService sysJobService;
|
||||
|
||||
/** 系统账号服务。 */
|
||||
private final SysAccountService sysAccountService;
|
||||
|
||||
/** 工作流使用权限校验服务。 */
|
||||
private final WorkflowUsageAuthorizationService workflowUsageAuthorizationService;
|
||||
|
||||
/** 工作流执行器。 */
|
||||
private final ChainExecutor chainExecutor;
|
||||
|
||||
/**
|
||||
* 创建工作流定时任务执行服务。
|
||||
*
|
||||
* @param sysJobService 定时任务服务
|
||||
* @param sysAccountService 系统账号服务
|
||||
* @param workflowUsageAuthorizationService 工作流使用权限校验服务
|
||||
* @param chainExecutor 工作流执行器
|
||||
*/
|
||||
public WorkflowJobExecutionService(
|
||||
SysJobService sysJobService,
|
||||
SysAccountService sysAccountService,
|
||||
WorkflowUsageAuthorizationService workflowUsageAuthorizationService,
|
||||
ChainExecutor chainExecutor) {
|
||||
this.sysJobService = sysJobService;
|
||||
this.sysAccountService = sysAccountService;
|
||||
this.workflowUsageAuthorizationService = workflowUsageAuthorizationService;
|
||||
this.chainExecutor = chainExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前数据库状态执行工作流定时任务。
|
||||
*
|
||||
* @param scheduledJob Quartz 中保存的任务快照
|
||||
* @return 工作流执行结果
|
||||
* @throws IllegalStateException 任务、账号或租户状态非法时抛出
|
||||
*/
|
||||
public Object execute(SysJob scheduledJob) {
|
||||
if (scheduledJob == null || scheduledJob.getId() == null) {
|
||||
throw new IllegalStateException("定时任务不存在或缺少ID");
|
||||
}
|
||||
return TenantManager.withoutTenantCondition(
|
||||
() -> executeWithoutTenantCondition(
|
||||
scheduledJob.getId(),
|
||||
scheduledJob.getTenantId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已关闭 ORM 租户条件的作用域中执行任务,并显式完成租户边界校验。
|
||||
*
|
||||
* @param jobId 定时任务 ID
|
||||
* @param scheduledTenantId Quartz 任务快照中的租户 ID
|
||||
* @return 工作流执行结果
|
||||
* @throws IllegalStateException 任务、账号或租户状态非法时抛出
|
||||
*/
|
||||
private Object executeWithoutTenantCondition(
|
||||
BigInteger jobId,
|
||||
BigInteger scheduledTenantId) {
|
||||
SysJob job = sysJobService.getById(jobId);
|
||||
if (job == null) {
|
||||
throw new IllegalStateException("定时任务不存在或已删除,id=" + jobId);
|
||||
}
|
||||
if (scheduledTenantId == null
|
||||
|| job.getTenantId() == null
|
||||
|| !Objects.equals(scheduledTenantId, job.getTenantId())) {
|
||||
throw new IllegalStateException("定时任务租户信息不一致,id=" + jobId);
|
||||
}
|
||||
if (!Integer.valueOf(EnumJobStatus.RUNNING.getCode()).equals(job.getStatus())) {
|
||||
throw new IllegalStateException("定时任务未处于运行状态,id=" + jobId);
|
||||
}
|
||||
if (!SysJobWorkflowReferenceSupport.isWorkflowJob(job)) {
|
||||
throw new IllegalStateException("定时任务类型已变更,id=" + jobId);
|
||||
}
|
||||
|
||||
SysAccount account = requireAvailableOwner(job);
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
BeanUtil.copyProperties(account, loginAccount);
|
||||
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(job);
|
||||
workflowUsageAuthorizationService.requireUsableWorkflow(
|
||||
workflowId,
|
||||
loginAccount,
|
||||
"定时任务关联的工作流不存在、已禁用或无权运行");
|
||||
|
||||
JSONObject workflowParams = resolveWorkflowParams(job.getJobParams());
|
||||
workflowParams.put(Constants.LOGIN_USER_KEY, loginAccount);
|
||||
return chainExecutor.execute(workflowId.toString(), workflowParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务创建账号并校验账号仍可用于执行任务。
|
||||
*
|
||||
* @param job 当前数据库中的定时任务
|
||||
* @return 可用的任务创建账号
|
||||
* @throws IllegalStateException 创建账号缺失、禁用或跨租户时抛出
|
||||
*/
|
||||
private SysAccount requireAvailableOwner(SysJob job) {
|
||||
BigInteger accountId = job.getCreatedBy();
|
||||
if (accountId == null) {
|
||||
throw new IllegalStateException("定时任务缺少服务端归属账号,id=" + job.getId());
|
||||
}
|
||||
SysAccount account = sysAccountService.getById(accountId);
|
||||
if (account == null) {
|
||||
throw new IllegalStateException("定时任务归属账号不存在,id=" + accountId);
|
||||
}
|
||||
if (!EnumDataStatus.AVAILABLE.getCode().equals(account.getStatus())) {
|
||||
throw new IllegalStateException("定时任务归属账号未启用,id=" + accountId);
|
||||
}
|
||||
if (!Objects.equals(job.getTenantId(), account.getTenantId())) {
|
||||
throw new IllegalStateException("定时任务与归属账号租户不一致,id=" + job.getId());
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工作流运行参数并返回可写对象。
|
||||
*
|
||||
* @param jobParams 定时任务参数
|
||||
* @return 工作流运行参数
|
||||
*/
|
||||
private JSONObject resolveWorkflowParams(Map<String, Object> jobParams) {
|
||||
if (jobParams == null) {
|
||||
return new JSONObject();
|
||||
}
|
||||
JSONObject params = new JSONObject(jobParams)
|
||||
.getJSONObject(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||
return params == null ? new JSONObject() : new JSONObject(params);
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,15 @@ package tech.easyflow.job.util;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import org.quartz.JobKey;
|
||||
import org.quartz.TriggerKey;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.job.service.WorkflowJobExecutionService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -68,34 +59,16 @@ public class JobUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过任务模块的受控执行服务运行工作流。
|
||||
*
|
||||
* @param job Quartz 中保存的任务快照
|
||||
* @return 工作流执行结果
|
||||
*/
|
||||
public static Object execWorkFlow(SysJob job) {
|
||||
Map<String, Object> jobParams = job.getJobParams();
|
||||
JSONObject obj = new JSONObject(jobParams);
|
||||
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(job);
|
||||
JSONObject params = obj.getJSONObject(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||
|
||||
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
|
||||
Object accountId = obj.get(JobConstant.ACCOUNT_ID);
|
||||
SysAccountService accountService = SpringContextUtil.getBean(SysAccountService.class);
|
||||
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
|
||||
ChainDefinition chain = executor.getDefinitionRepository().getChainDefinitionById(workflowId.toString());
|
||||
if (chain == null) {
|
||||
throw new IllegalStateException("定时任务关联的工作流不存在或已删除,id=" + workflowId);
|
||||
}
|
||||
if (accountId != null) {
|
||||
// 设置的归属者
|
||||
SysAccount account = accountService.getById(accountId.toString());
|
||||
if (account != null) {
|
||||
params.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
}
|
||||
return executor.execute(workflowId.toString(), params);
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
WorkflowJobExecutionService executionService =
|
||||
SpringContextUtil.getBean(WorkflowJobExecutionService.class);
|
||||
return executionService.execute(job);
|
||||
}
|
||||
|
||||
public static Object execute(SysJob job) {
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
package tech.easyflow.job.service;
|
||||
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.ai.service.WorkflowUsageAuthorizationService;
|
||||
import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
||||
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link WorkflowJobExecutionService} 运行时授权测试。
|
||||
*/
|
||||
public class WorkflowJobExecutionServiceTest {
|
||||
|
||||
/**
|
||||
* 验证每次触发都会按数据库当前状态授权,并将任务创建账号注入工作流参数。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReauthorizeAndRestoreServerControlledOwner() {
|
||||
BigInteger jobId = BigInteger.valueOf(101);
|
||||
BigInteger tenantId = BigInteger.valueOf(201);
|
||||
BigInteger accountId = BigInteger.valueOf(301);
|
||||
BigInteger workflowId = BigInteger.valueOf(401);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
WorkflowUsageAuthorizationService authorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
SysJob currentJob = workflowJob(jobId, tenantId, accountId, workflowId);
|
||||
SysAccount account = account(accountId, tenantId, EnumDataStatus.AVAILABLE.getCode());
|
||||
when(jobService.getById(jobId)).thenReturn(currentJob);
|
||||
when(accountService.getById(accountId)).thenReturn(account);
|
||||
Map<String, Object> executionResult = Map.of("status", "done");
|
||||
when(chainExecutor.execute(eq(workflowId.toString()), anyMap()))
|
||||
.thenReturn(executionResult);
|
||||
WorkflowJobExecutionService service = new WorkflowJobExecutionService(
|
||||
jobService,
|
||||
accountService,
|
||||
authorizationService,
|
||||
chainExecutor);
|
||||
|
||||
Object result = service.execute(scheduledJob(jobId, tenantId));
|
||||
|
||||
Assert.assertSame(executionResult, result);
|
||||
ArgumentCaptor<LoginAccount> accountCaptor =
|
||||
ArgumentCaptor.forClass(LoginAccount.class);
|
||||
verify(authorizationService).requireUsableWorkflow(
|
||||
eq(workflowId),
|
||||
accountCaptor.capture(),
|
||||
anyString());
|
||||
Assert.assertEquals(accountCaptor.getValue().getId(), accountId);
|
||||
Assert.assertEquals(accountCaptor.getValue().getTenantId(), tenantId);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, Object>> paramsCaptor =
|
||||
ArgumentCaptor.forClass(Map.class);
|
||||
verify(chainExecutor).execute(eq(workflowId.toString()), paramsCaptor.capture());
|
||||
Object loginUser = paramsCaptor.getValue().get(Constants.LOGIN_USER_KEY);
|
||||
Assert.assertTrue(loginUser instanceof LoginAccount);
|
||||
Assert.assertEquals(((LoginAccount) loginUser).getId(), accountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证权限已撤销时执行器不会启动工作流。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectExecutionAfterPermissionRevoked() {
|
||||
BigInteger jobId = BigInteger.valueOf(102);
|
||||
BigInteger tenantId = BigInteger.valueOf(202);
|
||||
BigInteger accountId = BigInteger.valueOf(302);
|
||||
BigInteger workflowId = BigInteger.valueOf(402);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
WorkflowUsageAuthorizationService authorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
when(jobService.getById(jobId))
|
||||
.thenReturn(workflowJob(jobId, tenantId, accountId, workflowId));
|
||||
when(accountService.getById(accountId))
|
||||
.thenReturn(account(accountId, tenantId, EnumDataStatus.AVAILABLE.getCode()));
|
||||
when(authorizationService.requireUsableWorkflow(
|
||||
eq(workflowId),
|
||||
any(LoginAccount.class),
|
||||
anyString()))
|
||||
.thenThrow(new BusinessException("工作流权限已撤销"));
|
||||
WorkflowJobExecutionService service = new WorkflowJobExecutionService(
|
||||
jobService,
|
||||
accountService,
|
||||
authorizationService,
|
||||
chainExecutor);
|
||||
|
||||
BusinessException exception = Assert.assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.execute(scheduledJob(jobId, tenantId))
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("权限已撤销"));
|
||||
verify(chainExecutor, never()).execute(anyString(), anyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证任务快照与数据库租户不一致时执行失败。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRejectCrossTenantJobSnapshot() {
|
||||
BigInteger jobId = BigInteger.valueOf(103);
|
||||
BigInteger tenantId = BigInteger.valueOf(203);
|
||||
SysJobService jobService = mock(SysJobService.class);
|
||||
SysAccountService accountService = mock(SysAccountService.class);
|
||||
WorkflowUsageAuthorizationService authorizationService =
|
||||
mock(WorkflowUsageAuthorizationService.class);
|
||||
ChainExecutor chainExecutor = mock(ChainExecutor.class);
|
||||
when(jobService.getById(jobId)).thenReturn(workflowJob(
|
||||
jobId,
|
||||
tenantId,
|
||||
BigInteger.valueOf(303),
|
||||
BigInteger.valueOf(403)));
|
||||
WorkflowJobExecutionService service = new WorkflowJobExecutionService(
|
||||
jobService,
|
||||
accountService,
|
||||
authorizationService,
|
||||
chainExecutor);
|
||||
|
||||
IllegalStateException exception = Assert.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> service.execute(scheduledJob(jobId, BigInteger.valueOf(999)))
|
||||
);
|
||||
|
||||
Assert.assertTrue(exception.getMessage().contains("租户"));
|
||||
verify(authorizationService, never()).requireUsableWorkflow(
|
||||
any(BigInteger.class),
|
||||
any(LoginAccount.class),
|
||||
anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Quartz 任务快照。
|
||||
*
|
||||
* @param jobId 定时任务 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @return 任务快照
|
||||
*/
|
||||
private SysJob scheduledJob(BigInteger jobId, BigInteger tenantId) {
|
||||
SysJob job = new SysJob();
|
||||
job.setId(jobId);
|
||||
job.setTenantId(tenantId);
|
||||
job.setJobType(EnumJobType.TINY_FLOW.getCode());
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建数据库中的工作流定时任务。
|
||||
*
|
||||
* @param jobId 定时任务 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param accountId 任务创建账号 ID
|
||||
* @param workflowId 工作流 ID
|
||||
* @return 工作流定时任务
|
||||
*/
|
||||
private SysJob workflowJob(
|
||||
BigInteger jobId,
|
||||
BigInteger tenantId,
|
||||
BigInteger accountId,
|
||||
BigInteger workflowId) {
|
||||
SysJob job = new SysJob();
|
||||
job.setId(jobId);
|
||||
job.setTenantId(tenantId);
|
||||
job.setCreatedBy(accountId);
|
||||
job.setStatus(EnumJobStatus.RUNNING.getCode());
|
||||
job.setJobType(EnumJobType.TINY_FLOW.getCode());
|
||||
job.setJobParams(Map.of(
|
||||
JobConstant.WORKFLOW_KEY, workflowId.toString(),
|
||||
JobConstant.WORKFLOW_PARAMS_KEY, Map.of("question", "hello")
|
||||
));
|
||||
return job;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建系统账号。
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param status 账号状态
|
||||
* @return 系统账号
|
||||
*/
|
||||
private SysAccount account(
|
||||
BigInteger accountId,
|
||||
BigInteger tenantId,
|
||||
Integer status) {
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
account.setTenantId(tenantId);
|
||||
account.setStatus(status);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -2,52 +2,38 @@ package tech.easyflow.system.config;
|
||||
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.dict.DictManager;
|
||||
import tech.easyflow.common.dict.loader.DbDataLoader;
|
||||
import tech.easyflow.system.entity.SysDict;
|
||||
import tech.easyflow.system.mapper.*;
|
||||
import tech.easyflow.system.service.SysDictService;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.EventListener;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 注册由字典表维护的静态业务字典。
|
||||
*/
|
||||
@Configuration
|
||||
public class SysDictAutoConfig {
|
||||
|
||||
private SysDictService service;
|
||||
|
||||
@Resource
|
||||
private SysMenuMapper sysMenuMapper;
|
||||
@Resource
|
||||
private SysDeptMapper sysDeptMapper;
|
||||
@Resource
|
||||
private SysRoleMapper sysRoleMapper;
|
||||
@Resource
|
||||
private SysPositionMapper sysPositionMapper;
|
||||
@Resource
|
||||
private SysAccountMapper sysAccountMapper;
|
||||
private final SysDictService service;
|
||||
|
||||
/**
|
||||
* 创建系统字典自动配置。
|
||||
*
|
||||
* @param service 系统字典服务
|
||||
*/
|
||||
public SysDictAutoConfig(SysDictService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用启动完成后注册静态字典。
|
||||
*/
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationStartup() {
|
||||
|
||||
DictManager dictManager = SpringContextUtil.getBean(DictManager.class);
|
||||
// 菜单表字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysMenu", sysMenuMapper, "id", "menu_title", "parent_id", "sort_no asc", false));
|
||||
// 部门表字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysDept", sysDeptMapper, "id", "dept_name", "parent_id", "sort_no asc", false));
|
||||
// 角色表字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysRole", sysRoleMapper, "id", "role_name", null, null, true));
|
||||
// 职位字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysPosition", sysPositionMapper, "id", "position_name", null, null, true));
|
||||
// 用户字典
|
||||
dictManager.putLoader(new DbDataLoader<>("sysAccount", sysAccountMapper, "id", "login_name", null, null, true));
|
||||
|
||||
List<SysDict> sysDicts = service.list();
|
||||
if (sysDicts != null) {
|
||||
sysDicts.forEach(sysDict -> dictManager.putLoader(sysDict.buildLoader()));
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
VALUES (
|
||||
399900000000000001, 366200000000000003, 1, '查询', '', '', '',
|
||||
0, '/api/v1/chatHistory/query', 1, 0, NOW(), 1, NOW(), 1, '聊天历史查询权限'
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT
|
||||
399900000000000100 + ROW_NUMBER() OVER (ORDER BY candidate.`role_id`),
|
||||
candidate.`role_id`,
|
||||
399900000000000001
|
||||
FROM (
|
||||
SELECT DISTINCT parent_grant.`role_id`
|
||||
FROM `tb_sys_role_menu` parent_grant
|
||||
WHERE parent_grant.`menu_id` = 366200000000000003
|
||||
) candidate
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `tb_sys_role_menu` existing_grant
|
||||
WHERE existing_grant.`role_id` = candidate.`role_id`
|
||||
AND existing_grant.`menu_id` = 399900000000000001
|
||||
);
|
||||
@@ -78,7 +78,7 @@ export interface AgentChatCapabilityPayload {
|
||||
}
|
||||
|
||||
export function getPublishedAgents() {
|
||||
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/options', {
|
||||
return api.get<RequestResult<AgentInfo[]>>('/api/v1/agent/session/options', {
|
||||
params: { publishedOnly: true },
|
||||
});
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export function getAgentSession(sessionId: number | string) {
|
||||
|
||||
export function getPublishedKnowledges() {
|
||||
return api.get<RequestResult<AgentChatKnowledgeView[]>>(
|
||||
'/api/v1/agent/knowledgeOptions',
|
||||
'/api/v1/agent/session/knowledgeOptions',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import pageSource from './index.vue?raw';
|
||||
|
||||
describe('管理端智能体聊天历史契约', () => {
|
||||
it('使用智能体候选接口并保留 assistantId 查询参数', () => {
|
||||
expect(pageSource).toContain("'/api/v1/agent/options'");
|
||||
expect(pageSource).toContain("'/api/v1/chatHistory/agentOptions'");
|
||||
expect(pageSource).not.toContain('/api/v1/bot/list');
|
||||
expect(pageSource).toContain('label: item.name');
|
||||
expect(pageSource).toContain('assistantId: query.value.assistantId');
|
||||
|
||||
@@ -103,7 +103,9 @@ onMounted(async () => {
|
||||
|
||||
async function fetchAgents() {
|
||||
agentLoading.value = true;
|
||||
const [error, res] = await tryit(api.get)('/api/v1/agent/options');
|
||||
const [error, res] = await tryit(api.get)(
|
||||
'/api/v1/chatHistory/agentOptions',
|
||||
);
|
||||
agentLoading.value = false;
|
||||
if (error || res?.errorCode !== 0) {
|
||||
agentOptions.value = [];
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
@@ -102,6 +101,8 @@ const normalizeEntity = (raw: any = {}) => {
|
||||
const entity = ref<any>(normalizeEntity(defaultEntity));
|
||||
|
||||
const btnLoading = ref(false);
|
||||
const categoryOptions = ref<any[]>([]);
|
||||
let categoryOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
collectionType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
@@ -145,7 +146,7 @@ const collectionTypeList = [
|
||||
},
|
||||
];
|
||||
|
||||
function openDialog(row: any = {}) {
|
||||
async function openDialog(row: any = {}) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
entity.value = normalizeEntity(row);
|
||||
@@ -154,6 +155,17 @@ function openDialog(row: any = {}) {
|
||||
entity.value = normalizeEntity(defaultEntity);
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
if (!categoryOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get(
|
||||
'/api/v1/documentCollectionCategory/visibleList',
|
||||
);
|
||||
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
categoryOptionsLoaded = true;
|
||||
} catch {
|
||||
categoryOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -251,10 +263,14 @@ defineExpose({
|
||||
prop="categoryId"
|
||||
:label="$t('documentCollection.categoryId')"
|
||||
>
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiDocumentCollectionCategory"
|
||||
/>
|
||||
<ElSelect v-model="entity.categoryId" clearable filterable>
|
||||
<ElOption
|
||||
v-for="category in categoryOptions"
|
||||
:key="category.id"
|
||||
:label="category.categoryName"
|
||||
:value="category.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
|
||||
@@ -375,15 +375,9 @@ const loadModels = async (preferredId?: string) => {
|
||||
|
||||
const loadPublishBaseUrl = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysOption/list', {
|
||||
params: {
|
||||
keys: ['chat_publish_base_url'],
|
||||
},
|
||||
});
|
||||
const res = await api.get('/api/v1/model/gatewayConfig');
|
||||
if (res.errorCode === 0) {
|
||||
publishBaseUrl.value = String(
|
||||
res.data?.chat_publish_base_url || '',
|
||||
).trim();
|
||||
publishBaseUrl.value = String(res.data?.publishBaseUrl || '').trim();
|
||||
}
|
||||
} catch {
|
||||
publishBaseUrl.value = '';
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElMessage} from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import {onMounted, ref} from 'vue';
|
||||
|
||||
@@ -37,6 +44,8 @@ const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const categoryOptions = ref<any[]>([]);
|
||||
let categoryOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
resourceType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
@@ -57,7 +66,7 @@ const rules = ref({
|
||||
});
|
||||
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
async function openDialog(row: any) {
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
@@ -65,6 +74,15 @@ function openDialog(row: any) {
|
||||
status: row?.status ?? 0,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
if (!categoryOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/resourceCategory/visibleList');
|
||||
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
categoryOptionsLoaded = true;
|
||||
} catch {
|
||||
categoryOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
@@ -158,10 +176,14 @@ function uploadError() {
|
||||
<ElInput v-model.trim="entity.resourceName" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiResource.categoryId')">
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiResourceCategory"
|
||||
/>
|
||||
<ElSelect v-model="entity.categoryId" clearable filterable>
|
||||
<ElOption
|
||||
v-for="category in categoryOptions"
|
||||
:key="category.id"
|
||||
:label="category.categoryName"
|
||||
:value="category.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
|
||||
@@ -151,9 +151,7 @@ async function initializeWorkflow() {
|
||||
}
|
||||
await Promise.all([
|
||||
loadCustomNode(),
|
||||
getLlmList(),
|
||||
getKnowledgeList(),
|
||||
getCodeEngineList(),
|
||||
getDesignerOptions(),
|
||||
getWorkflowInfo(workflowId.value),
|
||||
]);
|
||||
showTinyFlow.value = true;
|
||||
@@ -574,24 +572,14 @@ function reconcileWorkflowDraftAfterSave(savedContentSignature: string) {
|
||||
pendingDraftContent = normalizedContent;
|
||||
persistPendingWorkflowDraft();
|
||||
}
|
||||
async function getLlmList() {
|
||||
return api.get('/api/v1/model/list').then((res) => {
|
||||
llmList.value = res.data;
|
||||
});
|
||||
}
|
||||
async function getKnowledgeList() {
|
||||
return api.get('/api/v1/documentCollection/list').then((res) => {
|
||||
knowledgeList.value = res.data;
|
||||
});
|
||||
}
|
||||
async function getCodeEngineList() {
|
||||
return api.get('/api/v1/workflow/supportedCodeEngines').then((res) => {
|
||||
if (
|
||||
res?.errorCode === 0 &&
|
||||
Array.isArray(res.data) &&
|
||||
res.data.length > 0
|
||||
) {
|
||||
codeEngineList.value = res.data;
|
||||
async function getDesignerOptions() {
|
||||
return api.get('/api/v1/workflow/designer/options').then((res) => {
|
||||
llmList.value = Array.isArray(res.data?.models) ? res.data.models : [];
|
||||
knowledgeList.value = Array.isArray(res.data?.knowledges)
|
||||
? res.data.knowledges
|
||||
: [];
|
||||
if (Array.isArray(res.data?.codeEngines) && res.data.codeEngines.length > 0) {
|
||||
codeEngineList.value = res.data.codeEngines;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -785,7 +773,7 @@ function handleChoose(nodeName: string, value: any) {
|
||||
function handleWorkflowNodeUpdate(chooseId: any) {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/api/v1/workflowNode/getChainParams', {
|
||||
.get('/api/v1/workflow/designer/childWorkflow', {
|
||||
params: {
|
||||
currentId: workflowId.value,
|
||||
workflowId: chooseId,
|
||||
@@ -799,7 +787,7 @@ function handleWorkflowNodeUpdate(chooseId: any) {
|
||||
function handlePluginNodeUpdate(chooseId: any) {
|
||||
pageLoading.value = true;
|
||||
api
|
||||
.get('/api/v1/pluginItem/getTinyFlowData', {
|
||||
.get('/api/v1/workflow/designer/pluginTinyFlow', {
|
||||
params: {
|
||||
id: chooseId,
|
||||
},
|
||||
@@ -848,7 +836,7 @@ function onAsyncExecute(info: any) {
|
||||
:title="$t('menus.ai.plugin')"
|
||||
width="730"
|
||||
ref="pluginSelectRef"
|
||||
page-url="/api/v1/plugin/page?availableOnly=true"
|
||||
page-url="/api/v1/workflow/designer/plugins"
|
||||
:has-parent="true"
|
||||
single-select
|
||||
@get-data="(v) => handleChoose(nodeNames.pluginNode, v)"
|
||||
|
||||
@@ -201,7 +201,7 @@ const actions: ActionButton[] = [
|
||||
icon: Share,
|
||||
text: $t('button.share'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'inline',
|
||||
placement: 'menu',
|
||||
disabled: (row: any) =>
|
||||
row.publishStatus !== 'PUBLISHED' || sharingWorkflowId.value === row.id,
|
||||
loading: (row: any) => sharingWorkflowId.value === row.id,
|
||||
|
||||
@@ -49,6 +49,8 @@ const createDefaultEntity = () => ({
|
||||
});
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const categoryOptions = ref<any[]>([]);
|
||||
let categoryOptionsLoaded = false;
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
@@ -88,7 +90,7 @@ const rules = computed(() => ({
|
||||
}),
|
||||
}));
|
||||
// functions
|
||||
function openDialog(row: any, importMode = false) {
|
||||
async function openDialog(row: any, importMode = false) {
|
||||
isImport.value = importMode;
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
@@ -96,6 +98,15 @@ function openDialog(row: any, importMode = false) {
|
||||
...row,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
if (!categoryOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/workflowCategory/visibleList');
|
||||
categoryOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
categoryOptionsLoaded = true;
|
||||
} catch {
|
||||
categoryOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||
@@ -227,10 +238,14 @@ function closeDialog() {
|
||||
<ElInput v-model.trim="entity.title" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="categoryId" :label="$t('aiWorkflow.categoryId')">
|
||||
<DictSelect
|
||||
v-model="entity.categoryId"
|
||||
dict-code="aiWorkFlowCategory"
|
||||
/>
|
||||
<ElSelect v-model="entity.categoryId" clearable filterable>
|
||||
<ElOption
|
||||
v-for="category in categoryOptions"
|
||||
:key="category.id"
|
||||
:label="category.categoryName"
|
||||
:value="category.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
|
||||
@@ -76,17 +76,12 @@ function dedupeManagedDatasetOptions(options: ManagedDatasetOption[]) {
|
||||
export async function loadManagedDatasetOptions(): Promise<
|
||||
ManagedDatasetOption[]
|
||||
> {
|
||||
const sourceRes = await api.get('/api/v1/datacenterSource/page', {
|
||||
params: {
|
||||
pageNumber: 1,
|
||||
pageSize: 200,
|
||||
},
|
||||
});
|
||||
const sources = sourceRes.data?.records || [];
|
||||
const sourceRes = await api.get('/api/v1/workflow/designer/dataSources');
|
||||
const sources = sourceRes.data || [];
|
||||
const options: ManagedDatasetOption[] = [];
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const catalogRes = await api.get('/api/v1/datacenterSource/catalogs', {
|
||||
const catalogRes = await api.get('/api/v1/workflow/designer/catalogs', {
|
||||
params: {
|
||||
sourceId: source.id,
|
||||
},
|
||||
@@ -94,7 +89,7 @@ export async function loadManagedDatasetOptions(): Promise<
|
||||
const catalogs = catalogRes.data || [];
|
||||
for (const catalog of catalogs) {
|
||||
const tableRes = await api.get(
|
||||
'/api/v1/datacenterDataset/managedTables',
|
||||
'/api/v1/workflow/designer/managedTables',
|
||||
{
|
||||
params: {
|
||||
sourceId: source.id,
|
||||
@@ -176,7 +171,7 @@ export async function loadManagedDatasetSchema(
|
||||
fields: [],
|
||||
};
|
||||
}
|
||||
const res = await api.get('/api/v1/datacenterDataset/schema', {
|
||||
const res = await api.get('/api/v1/workflow/designer/schema', {
|
||||
params: datasetRef,
|
||||
});
|
||||
const data = res.data || {};
|
||||
@@ -188,8 +183,8 @@ export async function loadManagedDatasetSchema(
|
||||
}))
|
||||
: [];
|
||||
return {
|
||||
tableName: data.table?.tableName || datasetRef.tableName,
|
||||
tableDesc: data.table?.tableDesc,
|
||||
tableName: data.tableName || datasetRef.tableName,
|
||||
tableDesc: data.tableDesc,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,18 +28,6 @@ interface ProviderOption {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ModelProvider {
|
||||
key: string;
|
||||
options?: ProviderOptionExtra;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface LlmOption {
|
||||
extra: Map<string, string | undefined>;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface SettingsEntity {
|
||||
chatgpt_api_key: string;
|
||||
chatgpt_chatPath: string;
|
||||
@@ -50,18 +38,6 @@ interface SettingsEntity {
|
||||
}
|
||||
|
||||
const providerOptions = ref<ProviderOption[]>(providerList as ProviderOption[]);
|
||||
const brands = ref<ModelProvider[]>([]);
|
||||
const llmOptions = ref<LlmOption[]>([]);
|
||||
|
||||
// 获取品牌接口数据
|
||||
function getBrands() {
|
||||
api.get('/api/v1/modelProvider/list').then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
brands.value = (res.data ?? []) as ModelProvider[];
|
||||
llmOptions.value = formatLlmList(brands.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getOptions() {
|
||||
api
|
||||
.get(
|
||||
@@ -78,7 +54,6 @@ function getOptions() {
|
||||
}
|
||||
onMounted(() => {
|
||||
getOptions();
|
||||
getBrands();
|
||||
});
|
||||
|
||||
const entity = ref<SettingsEntity>({
|
||||
@@ -90,19 +65,6 @@ const entity = ref<SettingsEntity>({
|
||||
chat_publish_base_url: '',
|
||||
});
|
||||
|
||||
function formatLlmList(data: ModelProvider[]): LlmOption[] {
|
||||
return data.map((item) => {
|
||||
const extra = new Map([
|
||||
['chatPath', item.options?.chatPath],
|
||||
['llmEndpoint', item.options?.llmEndpoint],
|
||||
]);
|
||||
return {
|
||||
label: item.title,
|
||||
value: item.key,
|
||||
extra,
|
||||
};
|
||||
});
|
||||
}
|
||||
function handleChangeModel(value: string) {
|
||||
const extra = providerList.find((item) => item.value === value);
|
||||
entity.value.chatgpt_chatPath = extra?.options?.chatPath ?? '';
|
||||
|
||||
@@ -4,7 +4,7 @@ import pageSource from './index.vue?raw';
|
||||
|
||||
describe('管理端工作台智能体数据契约', () => {
|
||||
it('使用 Agent 候选接口和名称字段', () => {
|
||||
expect(pageSource).toContain("'/api/v1/agent/options'");
|
||||
expect(pageSource).toContain("'/api/v1/dashboard/agentOptions'");
|
||||
expect(pageSource).not.toContain('/api/v1/bot/list');
|
||||
expect(pageSource).toContain('label: item.name');
|
||||
expect(pageSource).not.toContain('publishedOnly');
|
||||
|
||||
@@ -332,7 +332,7 @@ async function loadAssistantOptions() {
|
||||
try {
|
||||
const agents =
|
||||
await requestClient.get<Array<{ id?: number | string; name?: string }>>(
|
||||
'/api/v1/agent/options',
|
||||
'/api/v1/dashboard/agentOptions',
|
||||
);
|
||||
const nextOptions: AssistantOptionItem[] = [
|
||||
{ label: '全部智能体', value: '' },
|
||||
|
||||
@@ -114,14 +114,14 @@ const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
|
||||
{ label: $t('approval.assignee.user'), value: 'USER' },
|
||||
{ label: $t('approval.assignee.dept'), value: 'DEPT' },
|
||||
];
|
||||
const ENABLED_DATA_STATUS = 1;
|
||||
const ASSIGNEE_VISIBLE_TAG_COUNT = 2;
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const categoryLoaded = ref(false);
|
||||
const resourceScopeOptionsLoaded = ref(false);
|
||||
let resourceScopeOptionsRequest: null | Promise<void> = null;
|
||||
const roleLoaded = ref(false);
|
||||
const accountLoading = ref(false);
|
||||
const deptTreeOptions = ref<any[]>([]);
|
||||
@@ -178,7 +178,7 @@ watch(
|
||||
return scope;
|
||||
});
|
||||
if (resourceType) {
|
||||
void ensureCategoryOptions();
|
||||
void ensureResourceScopeOptions();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -212,8 +212,7 @@ async function openDialog(row: any = {}) {
|
||||
formModel.value = buildDefaultForm();
|
||||
dialogVisible.value = true;
|
||||
await Promise.all([
|
||||
ensureCategoryOptions(),
|
||||
ensureDeptOptions(),
|
||||
ensureResourceScopeOptions(),
|
||||
ensureRoleOptions(),
|
||||
]);
|
||||
if (!row?.id) {
|
||||
@@ -276,40 +275,46 @@ async function openDialog(row: any = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCategoryOptions() {
|
||||
if (categoryLoaded.value) {
|
||||
async function ensureResourceScopeOptions() {
|
||||
if (resourceScopeOptionsLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const [agentRes, workflowRes, knowledgeRes] = await Promise.all([
|
||||
api.get('/api/v1/agentCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/documentCollectionCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
]);
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
|
||||
BOT: [],
|
||||
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
|
||||
};
|
||||
categoryLoaded.value = true;
|
||||
}
|
||||
|
||||
async function ensureDeptOptions() {
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
sortKey: 'sortNo',
|
||||
sortType: 'asc',
|
||||
status: ENABLED_DATA_STATUS,
|
||||
},
|
||||
});
|
||||
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
if (!resourceScopeOptionsRequest) {
|
||||
resourceScopeOptionsRequest = api
|
||||
.get('/api/v1/approvalFlow/resourceScopeOptions')
|
||||
.then((res) => {
|
||||
const categories = res.data?.categories || {};
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(categories.AGENT, 'categoryName'),
|
||||
BOT: [],
|
||||
KNOWLEDGE: normalizeCategoryOptions(
|
||||
categories.KNOWLEDGE,
|
||||
'categoryName',
|
||||
),
|
||||
WORKFLOW: normalizeCategoryOptions(
|
||||
categories.WORKFLOW,
|
||||
'categoryName',
|
||||
),
|
||||
};
|
||||
deptTreeOptions.value = Array.isArray(res.data?.departments)
|
||||
? res.data.departments
|
||||
: [];
|
||||
resourceScopeOptionsLoaded.value = true;
|
||||
})
|
||||
.catch(() => {
|
||||
categoryOptions.value = {
|
||||
AGENT: [],
|
||||
BOT: [],
|
||||
KNOWLEDGE: [],
|
||||
WORKFLOW: [],
|
||||
};
|
||||
deptTreeOptions.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
resourceScopeOptionsRequest = null;
|
||||
});
|
||||
}
|
||||
await resourceScopeOptionsRequest;
|
||||
}
|
||||
|
||||
async function ensureRoleOptions() {
|
||||
|
||||
@@ -5,7 +5,15 @@ import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { getCredentialKeyApi } from '#/api';
|
||||
import { api } from '#/api/request';
|
||||
@@ -26,6 +34,10 @@ const saveForm = ref<FormInstance>();
|
||||
// variables
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const departmentOptions = ref<any[]>([]);
|
||||
const roleOptions = ref<any[]>([]);
|
||||
const positionOptions = ref<any[]>([]);
|
||||
let formOptionsLoaded = false;
|
||||
function createDefaultEntity() {
|
||||
return {
|
||||
deptId: '',
|
||||
@@ -110,7 +122,7 @@ const rules = ref({
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
async function openDialog(row: any) {
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
@@ -123,6 +135,23 @@ function openDialog(row: any) {
|
||||
entity.value.positionIds = [];
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
if (!formOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysAccount/formOptions');
|
||||
departmentOptions.value = Array.isArray(res.data?.departments)
|
||||
? res.data.departments
|
||||
: [];
|
||||
roleOptions.value = Array.isArray(res.data?.roles) ? res.data.roles : [];
|
||||
positionOptions.value = Array.isArray(res.data?.positions)
|
||||
? res.data.positions
|
||||
: [];
|
||||
formOptionsLoaded = true;
|
||||
} catch {
|
||||
departmentOptions.value = [];
|
||||
roleOptions.value = [];
|
||||
positionOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
@@ -197,7 +226,14 @@ watch(
|
||||
<UploadAvatar v-model="entity.avatar" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="deptId" :label="$t('sysAccount.deptId')">
|
||||
<DictSelect v-model="entity.deptId" dict-code="sysDept" />
|
||||
<ElTreeSelect
|
||||
v-model="entity.deptId"
|
||||
:data="departmentOptions"
|
||||
:props="{ label: 'deptName', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="loginName" :label="$t('sysAccount.loginName')">
|
||||
<ElInput v-model.trim="entity.loginName" />
|
||||
@@ -244,14 +280,24 @@ watch(
|
||||
:label="$t('sysAccount.roleIds')"
|
||||
:required="isAdd"
|
||||
>
|
||||
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
|
||||
<ElSelect v-model="entity.roleIds" multiple clearable filterable>
|
||||
<ElOption
|
||||
v-for="role in roleOptions"
|
||||
:key="role.id"
|
||||
:label="role.roleName"
|
||||
:value="role.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
|
||||
<DictSelect
|
||||
multiple
|
||||
v-model="entity.positionIds"
|
||||
dict-code="sysPosition"
|
||||
/>
|
||||
<ElSelect v-model="entity.positionIds" multiple clearable filterable>
|
||||
<ElOption
|
||||
v-for="position in positionOptions"
|
||||
:key="position.id"
|
||||
:label="position.positionName"
|
||||
:value="position.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
|
||||
@@ -5,7 +5,13 @@ import { ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
@@ -22,6 +28,8 @@ const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>(buildDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const parentDepartmentOptions = ref<any[]>([]);
|
||||
let parentDepartmentOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
parentId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
@@ -45,13 +53,31 @@ function buildDefaultEntity() {
|
||||
status: ENABLED_STATUS,
|
||||
};
|
||||
}
|
||||
function openDialog(row: any = {}) {
|
||||
async function openDialog(row: any = {}) {
|
||||
isAdd.value = !row.id;
|
||||
entity.value = {
|
||||
...buildDefaultEntity(),
|
||||
...row,
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
if (!parentDepartmentOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
sortKey: 'sortNo',
|
||||
sortType: 'asc',
|
||||
},
|
||||
});
|
||||
parentDepartmentOptions.value = [
|
||||
{ id: 0, deptName: $t('sysDept.root'), children: [] },
|
||||
...(Array.isArray(res.data) ? res.data : []),
|
||||
];
|
||||
parentDepartmentOptionsLoaded = true;
|
||||
} catch {
|
||||
parentDepartmentOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
if (btnLoading.value) {
|
||||
@@ -104,11 +130,14 @@ function closeDialog() {
|
||||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||
>
|
||||
<ElFormItem prop="parentId" :label="$t('sysDept.parentId')">
|
||||
<DictSelect
|
||||
<ElTreeSelect
|
||||
:disabled="entity.deptCode === 'root_dept'"
|
||||
:extra-options="[{ label: $t('sysDept.root'), value: 0 }]"
|
||||
v-model="entity.parentId"
|
||||
dict-code="sysDept"
|
||||
:data="parentDepartmentOptions"
|
||||
:props="{ label: 'deptName', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="deptName" :label="$t('sysDept.deptName')">
|
||||
|
||||
@@ -78,7 +78,14 @@ vi.mock('element-plus', () => ({
|
||||
},
|
||||
}),
|
||||
ElInput: defineComponent({ name: 'ElInput', setup: () => () => h('input') }),
|
||||
ElMessage: { success: vi.fn() },
|
||||
ElMessage: { error: vi.fn(), success: vi.fn() },
|
||||
ElOption: defineComponent({ name: 'ElOption', setup: () => () => h('div') }),
|
||||
ElSelect: defineComponent({
|
||||
name: 'ElSelect',
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', slots.default?.());
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('sys job modal', () => {
|
||||
@@ -112,7 +119,8 @@ describe('sys job modal', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(apiMocks.get).toHaveBeenCalledWith(
|
||||
'/api/v1/workflow/getRunningParameters?id=101',
|
||||
'/api/v1/sysJob/workflowRunningParameters',
|
||||
{ params: { id: '101' } },
|
||||
);
|
||||
expect(wrapper.get('form').attributes('data-loading')).toBe('false');
|
||||
expect(wrapper.text()).toContain('所选工作流已不可用,请重新选择');
|
||||
|
||||
@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElAlert, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import {
|
||||
ElAlert,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import CronPicker from '#/components/cron/CronPicker.vue';
|
||||
@@ -39,6 +47,8 @@ const initEntity = {
|
||||
};
|
||||
const entity = ref<any>(initEntity);
|
||||
const btnLoading = ref(false);
|
||||
const workflowOptions = ref<any[]>([]);
|
||||
let workflowOptionsLoaded = false;
|
||||
// 基础验证规则
|
||||
const baseRules = ref({
|
||||
jobName: [
|
||||
@@ -85,8 +95,22 @@ function openDialog(row: any) {
|
||||
} else {
|
||||
entity.value = { ...initEntity };
|
||||
}
|
||||
void ensureWorkflowOptions();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
async function ensureWorkflowOptions() {
|
||||
if (workflowOptionsLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysJob/workflowOptions');
|
||||
workflowOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
workflowOptionsLoaded = true;
|
||||
} catch {
|
||||
workflowOptions.value = [];
|
||||
ElMessage.error('工作流列表加载失败');
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
if (btnLoading.value || workflowParamsSubmissionBlocked.value) {
|
||||
return;
|
||||
@@ -146,7 +170,9 @@ async function getWorkflowParams(v: any) {
|
||||
workflowParams.value = [];
|
||||
workflowParamsLoadError.value = '';
|
||||
try {
|
||||
const res = await api.get(`/api/v1/workflow/getRunningParameters?id=${v}`);
|
||||
const res = await api.get('/api/v1/sysJob/workflowRunningParameters', {
|
||||
params: { id: v },
|
||||
});
|
||||
if (requestId !== workflowParamsRequestId) {
|
||||
return;
|
||||
}
|
||||
@@ -209,11 +235,19 @@ const str = '"param"';
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
]"
|
||||
>
|
||||
<DictSelect
|
||||
<ElSelect
|
||||
v-model="entity.jobParams.workflowId"
|
||||
dict-code="aiWorkFlow"
|
||||
clearable
|
||||
filterable
|
||||
@change="workflowChange"
|
||||
/>
|
||||
>
|
||||
<ElOption
|
||||
v-for="workflow in workflowOptions"
|
||||
:key="workflow.id"
|
||||
:label="workflow.title"
|
||||
:value="workflow.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElAlert
|
||||
v-if="workflowParamsLoadError"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
@@ -44,6 +45,8 @@ const entity = ref<any>({
|
||||
remark: '',
|
||||
});
|
||||
const btnLoading = ref(false);
|
||||
const parentMenuOptions = ref<any[]>([]);
|
||||
let parentMenuOptionsLoaded = false;
|
||||
const rules = ref({
|
||||
parentId: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
@@ -62,12 +65,26 @@ const rules = ref({
|
||||
],
|
||||
});
|
||||
// functions
|
||||
function openDialog(row: any) {
|
||||
async function openDialog(row: any) {
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
dialogVisible.value = true;
|
||||
if (!parentMenuOptionsLoaded) {
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysMenu/list', {
|
||||
params: { asTree: true },
|
||||
});
|
||||
parentMenuOptions.value = [
|
||||
{ id: 0, menuTitle: $t('sysMenu.root'), children: [] },
|
||||
...(Array.isArray(res.data) ? res.data : []),
|
||||
];
|
||||
parentMenuOptionsLoaded = true;
|
||||
} catch {
|
||||
parentMenuOptions.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
@@ -121,10 +138,13 @@ function closeDialog() {
|
||||
class="easyflow-modal-form easyflow-modal-form--compact"
|
||||
>
|
||||
<ElFormItem prop="parentId" :label="$t('sysMenu.parentId')">
|
||||
<DictSelect
|
||||
:extra-options="[{ label: $t('sysMenu.root'), value: 0 }]"
|
||||
<ElTreeSelect
|
||||
v-model="entity.parentId"
|
||||
dict-code="sysMenu"
|
||||
:data="parentMenuOptions"
|
||||
:props="{ label: 'menuTitle', value: 'id', children: 'children' }"
|
||||
check-strictly
|
||||
clearable
|
||||
filterable
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="menuType" :label="$t('sysMenu.menuType')">
|
||||
|
||||
@@ -19,7 +19,6 @@ defineExpose({
|
||||
|
||||
type ResourceType =
|
||||
| 'AGENT'
|
||||
| 'BOT'
|
||||
| 'KNOWLEDGE'
|
||||
| 'PLUGIN'
|
||||
| 'RESOURCE'
|
||||
@@ -47,7 +46,6 @@ const RESOURCE_SCOPE_GROUPS: Array<{
|
||||
resourceType: ResourceType;
|
||||
}> = [
|
||||
{ resourceType: 'AGENT', label: $t('menus.ai.agents') },
|
||||
{ resourceType: 'BOT', label: $t('bot.chatAssistant') },
|
||||
{ resourceType: 'PLUGIN', label: $t('menus.ai.plugin') },
|
||||
{ resourceType: 'WORKFLOW', label: $t('menus.ai.workflow') },
|
||||
{ resourceType: 'KNOWLEDGE', label: $t('menus.ai.documentCollection') },
|
||||
@@ -58,11 +56,11 @@ const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const entity = ref<any>(buildDefaultEntity());
|
||||
const categoryScopeLoaded = ref(false);
|
||||
const formOptionsLoaded = ref(false);
|
||||
const menuOptions = ref<any[]>([]);
|
||||
const categoryScopeEditable = ref(false);
|
||||
const categoryOptions = ref<Record<ResourceType, CategoryOption[]>>({
|
||||
AGENT: [],
|
||||
BOT: [],
|
||||
KNOWLEDGE: [],
|
||||
PLUGIN: [],
|
||||
RESOURCE: [],
|
||||
@@ -135,7 +133,7 @@ function openDialog(row: any = {}) {
|
||||
getMenuIds(row.id);
|
||||
}
|
||||
getCategoryScopeDetail(row.id);
|
||||
void ensureCategoryOptions();
|
||||
void ensureFormOptions();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -189,40 +187,32 @@ function getMenuIds(roleId: any) {
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureCategoryOptions() {
|
||||
if (categoryScopeLoaded.value) {
|
||||
async function ensureFormOptions() {
|
||||
if (formOptionsLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const requests = [
|
||||
api.get('/api/v1/agentCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/botCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/pluginCategory/list'),
|
||||
api.get('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/documentCollectionCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/resourceCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
];
|
||||
|
||||
const [agentRes, botRes, pluginRes, workflowRes, knowledgeRes, resourceRes] =
|
||||
await Promise.all(requests);
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(agentRes.data, 'categoryName'),
|
||||
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
|
||||
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
|
||||
PLUGIN: normalizeCategoryOptions(pluginRes.data, 'name'),
|
||||
RESOURCE: normalizeCategoryOptions(resourceRes.data, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
|
||||
};
|
||||
categoryScopeLoaded.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/sysRole/formOptions');
|
||||
const categories = res.data?.categories || {};
|
||||
menuOptions.value = Array.isArray(res.data?.menus) ? res.data.menus : [];
|
||||
categoryOptions.value = {
|
||||
AGENT: normalizeCategoryOptions(categories.AGENT, 'categoryName'),
|
||||
KNOWLEDGE: normalizeCategoryOptions(categories.KNOWLEDGE, 'categoryName'),
|
||||
PLUGIN: normalizeCategoryOptions(categories.PLUGIN, 'categoryName'),
|
||||
RESOURCE: normalizeCategoryOptions(categories.RESOURCE, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(categories.WORKFLOW, 'categoryName'),
|
||||
};
|
||||
formOptionsLoaded.value = true;
|
||||
} catch {
|
||||
menuOptions.value = [];
|
||||
categoryOptions.value = {
|
||||
AGENT: [],
|
||||
KNOWLEDGE: [],
|
||||
PLUGIN: [],
|
||||
RESOURCE: [],
|
||||
WORKFLOW: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCategoryOptions(data: any[] = [], labelKey: string) {
|
||||
@@ -296,18 +286,24 @@ function getCategoryScopeDetail(roleId: number | string) {
|
||||
categoryScopeEditable.value = !!res.data?.editable;
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
scopes: (res.data?.scopes || buildDefaultScopes()).map((item: any) => ({
|
||||
categoryIds: item.categoryIds || [],
|
||||
resourceType: item.resourceType,
|
||||
scopeMode: item.scopeMode || 'CUSTOM',
|
||||
})),
|
||||
scopes: (res.data?.scopes || buildDefaultScopes())
|
||||
.filter((item: any) =>
|
||||
RESOURCE_SCOPE_GROUPS.some(
|
||||
(group) => group.resourceType === item.resourceType,
|
||||
),
|
||||
)
|
||||
.map((item: any) => ({
|
||||
categoryIds: item.categoryIds || [],
|
||||
resourceType: item.resourceType,
|
||||
scopeMode: item.scopeMode || 'CUSTOM',
|
||||
})),
|
||||
};
|
||||
syncCategoryTreeCheckedKeys(categoryScopeDetail.value.scopes);
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCategoryScope(roleId: number | string) {
|
||||
await ensureCategoryOptions();
|
||||
await ensureFormOptions();
|
||||
const scopes = buildScopeItemsFromTree();
|
||||
categoryScopeDetail.value = {
|
||||
roleId,
|
||||
@@ -353,7 +349,7 @@ async function saveCategoryScope(roleId: number | string) {
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="$t('sysRole.menuPermission')">
|
||||
<Tree
|
||||
data-url="/api/v1/sysMenu/list?asTree=true"
|
||||
:data="menuOptions"
|
||||
v-model="entity.menuIds"
|
||||
:default-props="{
|
||||
label: 'menuTitle',
|
||||
|
||||
Reference in New Issue
Block a user