feat: 工作流适配数据中枢查询节点
- 新增查询数据与写入数据节点并移除旧数据中心节点入口 - 将查询数据节点切换为连接服务加 SQL 的执行模型 - 同步更新工作流校验、提示词上下文与设计器交互
This commit is contained in:
@@ -13,6 +13,7 @@ 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.common.domain.Result;
|
||||
@@ -29,6 +30,8 @@ public class WorkFlowNodeController {
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
|
||||
@GetMapping("/getChainParams")
|
||||
public Result<?> getChainParams(String currentId, String workflowId) {
|
||||
@@ -43,7 +46,7 @@ public class WorkFlowNodeController {
|
||||
nodeData.put("workflowId", workflow.getId());
|
||||
nodeData.put("workflowName", workflow.getTitle());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
List<Node> nodes = definition.getNodes();
|
||||
JSONArray inputs = new JSONArray();
|
||||
JSONArray outputs = new JSONArray();
|
||||
|
||||
@@ -21,6 +21,7 @@ import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.BotWorkflowService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
@@ -76,6 +77,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
@@ -223,7 +226,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
if (definition == null) {
|
||||
return Result.fail(2, "节点配置错误,请检查! ");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
@@ -40,6 +41,8 @@ public class PublicWorkflowController {
|
||||
private TinyFlowService tinyFlowService;
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
|
||||
/**
|
||||
* 通过id或别名获取工作流详情
|
||||
@@ -123,7 +126,7 @@ public class PublicWorkflowController {
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
if (definition == null) {
|
||||
return Result.fail(2, "节点配置错误,请检查! ");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
@@ -53,6 +54,8 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
@Resource
|
||||
private WorkflowCheckService workflowCheckService;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
@Resource
|
||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||
|
||||
public UcWorkflowController(WorkflowService service) {
|
||||
@@ -168,7 +171,7 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
||||
}
|
||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||
|
||||
ChainDefinition definition = chainParser.parse(workflow.getContent());
|
||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
||||
if (definition == null) {
|
||||
return Result.fail(2, "节点配置错误,请检查! ");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository;
|
||||
import com.easyagents.flow.core.parser.ChainParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
|
||||
@@ -16,11 +17,13 @@ public class ChainDefinitionRepositoryImpl implements ChainDefinitionRepository
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
|
||||
@Override
|
||||
public ChainDefinition getChainDefinitionById(String id) {
|
||||
Workflow workflow = workflowService.getById(id);
|
||||
String json = workflow.getContent();
|
||||
String json = workflowDatacenterContentService.prepareContent(workflow.getContent());
|
||||
ChainDefinition chainDefinition = chainParser.parse(json);
|
||||
chainDefinition.setId(workflow.getId().toString());
|
||||
chainDefinition.setName(workflow.getEnglishName());
|
||||
|
||||
@@ -65,14 +65,12 @@ public class TinyFlowConfigService {
|
||||
MakeFileNodeParser makeFileNodeParser = new MakeFileNodeParser();
|
||||
// 插件
|
||||
PluginToolNodeParser pluginToolNodeParser = new PluginToolNodeParser();
|
||||
// SQL查询
|
||||
SqlNodeParser sqlNodeParser = new SqlNodeParser();
|
||||
// 下载文件节点
|
||||
DownloadNodeParser downloadNodeParser = new DownloadNodeParser();
|
||||
// 保存数据节点
|
||||
SaveToDatacenterNodeParser saveDaveParser = new SaveToDatacenterNodeParser();
|
||||
SaveDatasetNodeParser saveDatasetNodeParser = new SaveDatasetNodeParser();
|
||||
// 查询数据节点
|
||||
SearchDatacenterNodeParser searchDatacenterNodeParser = new SearchDatacenterNodeParser();
|
||||
SearchDatasetNodeParser searchDatasetNodeParser = new SearchDatasetNodeParser();
|
||||
// 工作流节点
|
||||
WorkflowNodeParser workflowNodeParser = new WorkflowNodeParser();
|
||||
// 条件判断节点
|
||||
@@ -81,10 +79,9 @@ public class TinyFlowConfigService {
|
||||
chainParser.addNodeParser(docNodeParser.getNodeName(), docNodeParser);
|
||||
chainParser.addNodeParser(makeFileNodeParser.getNodeName(), makeFileNodeParser);
|
||||
chainParser.addNodeParser(pluginToolNodeParser.getNodeName(), pluginToolNodeParser);
|
||||
chainParser.addNodeParser(sqlNodeParser.getNodeName(), sqlNodeParser);
|
||||
chainParser.addNodeParser(downloadNodeParser.getNodeName(), downloadNodeParser);
|
||||
chainParser.addNodeParser(saveDaveParser.getNodeName(), saveDaveParser);
|
||||
chainParser.addNodeParser(searchDatacenterNodeParser.getNodeName(), searchDatacenterNodeParser);
|
||||
chainParser.addNodeParser(saveDatasetNodeParser.getNodeName(), saveDatasetNodeParser);
|
||||
chainParser.addNodeParser(searchDatasetNodeParser.getNodeName(), searchDatasetNodeParser);
|
||||
chainParser.addNodeParser(workflowNodeParser.getNodeName(), workflowNodeParser);
|
||||
chainParser.addNodeParser(conditionNodeParser.getNodeName(), conditionNodeParser);
|
||||
}
|
||||
|
||||
@@ -12,15 +12,30 @@ import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSchemaResponse;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
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.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class WorkflowCheckService {
|
||||
private static final String LEVEL_ERROR = "ERROR";
|
||||
private static final String LEVEL_WARNING = "WARNING";
|
||||
private static final String TYPE_START = "startNode";
|
||||
private static final String TYPE_END = "endNode";
|
||||
private static final String TYPE_LOOP = "loopNode";
|
||||
@@ -30,6 +45,8 @@ public class WorkflowCheckService {
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private ChainParser chainParser;
|
||||
@Resource
|
||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||
|
||||
public WorkflowCheckResult checkWorkflow(BigInteger workflowId, WorkflowCheckStage stage) {
|
||||
if (workflowId == null) {
|
||||
@@ -177,9 +194,91 @@ public class WorkflowCheckService {
|
||||
parsedWorkflow.nodes = nodes;
|
||||
parsedWorkflow.edges = edges;
|
||||
parsedWorkflow.nodeMap = nodeMap;
|
||||
checkDatacenterNodes(parsedWorkflow, issues, issueKeys);
|
||||
return parsedWorkflow;
|
||||
}
|
||||
|
||||
private void checkDatacenterNodes(ParsedWorkflow parsed, List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
|
||||
for (NodeView node : parsed.nodes) {
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
if (workflowDatacenterContentService.isSearchDatasetNode(node.type)) {
|
||||
checkSearchDatasetNode(node, issues, issueKeys);
|
||||
continue;
|
||||
}
|
||||
if (workflowDatacenterContentService.isSaveDatasetNode(node.type)) {
|
||||
checkSaveDatasetNode(node, issues, issueKeys);
|
||||
continue;
|
||||
}
|
||||
if (workflowDatacenterContentService.isLlmNode(node.type)) {
|
||||
checkLlmQueryContext(node, parsed, issues, issueKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSearchDatasetNode(NodeView node,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
try {
|
||||
workflowDatacenterContentService.requireSearchDatasetRef(node.data);
|
||||
} catch (BusinessException e) {
|
||||
addIssue(issues, issueKeys, "SEARCH_DATASET_INVALID", e.getMessage(), node.id, null, node.name);
|
||||
} catch (Exception e) {
|
||||
addIssue(issues, issueKeys, "SEARCH_DATASET_INVALID",
|
||||
"查询数据节点校验失败: " + shortError(e), node.id, null, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSaveDatasetNode(NodeView node,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
try {
|
||||
workflowDatacenterContentService.requireSaveDatasetRef(node.data);
|
||||
} catch (BusinessException e) {
|
||||
addIssue(issues, issueKeys, "SAVE_DATASET_INVALID", e.getMessage(), node.id, null, node.name);
|
||||
} catch (Exception e) {
|
||||
addIssue(issues, issueKeys, "SAVE_DATASET_INVALID",
|
||||
"写入数据节点校验失败: " + shortError(e), node.id, null, node.name);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLlmQueryContext(NodeView node,
|
||||
ParsedWorkflow parsed,
|
||||
List<WorkflowCheckIssue> issues,
|
||||
Set<String> issueKeys) {
|
||||
if (node.data == null) {
|
||||
return;
|
||||
}
|
||||
Object rawNodeIds = node.data.get("queryContextNodeIds");
|
||||
if (rawNodeIds == null) {
|
||||
return;
|
||||
}
|
||||
if (!(rawNodeIds instanceof JSONArray nodeIds)) {
|
||||
addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_INVALID",
|
||||
"查询上下文配置无效,请重新选择查询数据节点", node.id, null, node.name);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < nodeIds.size(); i++) {
|
||||
String refNodeId = trimToNull(nodeIds.getString(i));
|
||||
if (!StringUtils.hasText(refNodeId)) {
|
||||
addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_EMPTY",
|
||||
"查询上下文配置无效,请重新选择查询数据节点", node.id, null, node.name);
|
||||
continue;
|
||||
}
|
||||
NodeView refNode = parsed.nodeMap.get(refNodeId);
|
||||
if (refNode == null) {
|
||||
addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_NOT_FOUND",
|
||||
"查询上下文节点不存在: " + refNodeId, node.id, null, node.name);
|
||||
continue;
|
||||
}
|
||||
if (!workflowDatacenterContentService.isSearchDatasetNode(refNode.type)) {
|
||||
addIssue(issues, issueKeys, "LLM_QUERY_CONTEXT_TYPE_INVALID",
|
||||
"查询上下文只能选择查询数据节点", node.id, null, node.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void runStrictChecks(String content, ParsedWorkflow parsed, BigInteger currentWorkflowId,
|
||||
List<WorkflowCheckIssue> issues, Set<String> issueKeys) {
|
||||
if (parsed.nodes.isEmpty()) {
|
||||
@@ -190,7 +289,8 @@ public class WorkflowCheckService {
|
||||
}
|
||||
|
||||
try {
|
||||
Object definition = chainParser.parse(content);
|
||||
String preparedContent = workflowDatacenterContentService.prepareContent(content);
|
||||
Object definition = chainParser.parse(preparedContent);
|
||||
if (definition == null) {
|
||||
addIssue(issues, issueKeys, "PARSE_NULL", "预执行校验失败:节点配置错误,请检查", null, null, null);
|
||||
}
|
||||
@@ -606,31 +706,47 @@ public class WorkflowCheckService {
|
||||
result.setStage(stage);
|
||||
result.setIssues(issues);
|
||||
result.setIssueCount(issues.size());
|
||||
result.setPassed(issues.isEmpty());
|
||||
result.setPassed(issues.stream().noneMatch(issue -> LEVEL_ERROR.equalsIgnoreCase(issue.getLevel())));
|
||||
return result;
|
||||
}
|
||||
|
||||
private void throwIfFailed(WorkflowCheckResult result) {
|
||||
if (result == null || result.isPassed()) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
String summary = result.getIssues().stream()
|
||||
List<WorkflowCheckIssue> errorIssues = result.getIssues().stream()
|
||||
.filter(issue -> LEVEL_ERROR.equalsIgnoreCase(issue.getLevel()))
|
||||
.collect(Collectors.toList());
|
||||
if (errorIssues.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String summary = errorIssues.stream()
|
||||
.limit(5)
|
||||
.map(WorkflowCheckIssue::getMessage)
|
||||
.collect(Collectors.joining(";"));
|
||||
if (result.getIssueCount() > 5) {
|
||||
if (errorIssues.size() > 5) {
|
||||
summary = summary + ";等";
|
||||
}
|
||||
throw new BusinessException("工作流校验未通过(" + result.getStage() + "),共 " + result.getIssueCount() + " 项:" + summary);
|
||||
throw new BusinessException("工作流校验未通过(" + result.getStage() + "),共 " + errorIssues.size() + " 项:" + summary);
|
||||
}
|
||||
|
||||
private void addIssue(List<WorkflowCheckIssue> issues, Set<String> issueKeys, String code,
|
||||
String message, String nodeId, String edgeId, String nodeName) {
|
||||
addIssue(issues, issueKeys, code, LEVEL_ERROR, message, nodeId, edgeId, nodeName);
|
||||
}
|
||||
|
||||
private void addWarning(List<WorkflowCheckIssue> issues, Set<String> issueKeys, String code,
|
||||
String message, String nodeId, String edgeId, String nodeName) {
|
||||
addIssue(issues, issueKeys, code, LEVEL_WARNING, message, nodeId, edgeId, nodeName);
|
||||
}
|
||||
|
||||
private void addIssue(List<WorkflowCheckIssue> issues, Set<String> issueKeys, String code, String level,
|
||||
String message, String nodeId, String edgeId, String nodeName) {
|
||||
String key = code + "|" + safe(nodeId) + "|" + safe(edgeId) + "|" + message;
|
||||
if (!issueKeys.add(key)) {
|
||||
return;
|
||||
}
|
||||
issues.add(new WorkflowCheckIssue(code, LEVEL_ERROR, message, nodeId, edgeId, nodeName));
|
||||
issues.add(new WorkflowCheckIssue(code, level, message, nodeId, edgeId, nodeName));
|
||||
}
|
||||
|
||||
private String extractNodeName(JSONObject nodeJson, JSONObject data, String fallback) {
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
package tech.easyflow.ai.easyagentsflow.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.common.constant.enums.EnumFieldType;
|
||||
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.DatacenterCatalog;
|
||||
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
|
||||
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class WorkflowDatacenterContentService {
|
||||
|
||||
public static final String SEARCH_NODE_TYPE = "search-dataset-node";
|
||||
public static final String SAVE_NODE_TYPE = "save-dataset-node";
|
||||
public static final String LLM_NODE_TYPE = "llmNode";
|
||||
public static final String QUERY_DATA_CONTEXT = "queryDataContext";
|
||||
public static final String SEARCH_SOURCE_MISSING_MESSAGE = "查询数据节点未选择连接服务";
|
||||
public static final String SEARCH_SQL_MISSING_MESSAGE = "查询数据节点未设置 SQL";
|
||||
public static final String SAVE_EXPIRED_MESSAGE = "写入数据节点配置已过期,请重新选择已接入表";
|
||||
public static final String INVALID_QUERY_CONTEXT_MESSAGE = "查询上下文配置无效,请重新选择查询数据节点";
|
||||
private static final String QUERY_CONTEXT_PROMPT = """
|
||||
你是为工作流中的查询数据节点生成只读 SQL 的生成器,你的职责是返回可直接执行的 SQL,并且你只能输出 SQL。
|
||||
|
||||
必须严格遵守以下规则:
|
||||
1. 只能从下面给出的连接摘要中选择最合适的表。
|
||||
2. 只能使用摘要中给出的字段名,不要虚构表名或字段名。
|
||||
3. 只输出 SQL,不要输出解释、注释、Markdown、JSON 或多余文本。
|
||||
4. 只能生成只读 SELECT SQL,允许 WITH、JOIN、子查询、聚合、分组、排序。
|
||||
5. 不要生成 INSERT、UPDATE、DELETE、DDL、多语句、存储过程调用。
|
||||
6. 优先使用逻辑表名和逻辑字段名,不要输出物理表名、JDBC、驱动信息。
|
||||
7. 如果存在重名表,请使用 catalog.table 形式消除歧义。
|
||||
|
||||
以下是可用的连接摘要:
|
||||
""";
|
||||
|
||||
@Resource
|
||||
private DatacenterDatasetQueryService queryService;
|
||||
@Resource
|
||||
private DatacenterDatasetRegistryService registryService;
|
||||
|
||||
public boolean isSearchDatasetNode(String nodeType) {
|
||||
return SEARCH_NODE_TYPE.equals(nodeType);
|
||||
}
|
||||
|
||||
public boolean isSaveDatasetNode(String nodeType) {
|
||||
return SAVE_NODE_TYPE.equals(nodeType);
|
||||
}
|
||||
|
||||
public boolean isLlmNode(String nodeType) {
|
||||
return LLM_NODE_TYPE.equals(nodeType);
|
||||
}
|
||||
|
||||
public String prepareContent(String content) {
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new BusinessException("工作流内容不能为空");
|
||||
}
|
||||
Object parsed;
|
||||
try {
|
||||
parsed = JSON.parse(content);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("工作流内容不是合法JSON: " + shortError(e));
|
||||
}
|
||||
if (!(parsed instanceof JSONObject root)) {
|
||||
throw new BusinessException("工作流内容必须是JSON对象");
|
||||
}
|
||||
JSONObject copy = JSON.parseObject(root.toJSONString());
|
||||
prepareRoot(copy);
|
||||
return copy.toJSONString();
|
||||
}
|
||||
|
||||
public JSONObject prepareRoot(JSONObject root) {
|
||||
if (root == null) {
|
||||
throw new BusinessException("工作流内容不能为空");
|
||||
}
|
||||
JSONArray nodes = root.getJSONArray("nodes");
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
return root;
|
||||
}
|
||||
Map<String, JSONObject> nodeMap = buildNodeMap(nodes);
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
String nodeType = node.getString("type");
|
||||
JSONObject data = node.getJSONObject("data");
|
||||
if (isSearchDatasetNode(nodeType)) {
|
||||
requireSearchDatasetRef(data);
|
||||
} else if (isSaveDatasetNode(nodeType)) {
|
||||
requireSaveDatasetRef(data);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null || !isLlmNode(node.getString("type"))) {
|
||||
continue;
|
||||
}
|
||||
injectQueryDataContext(node.getJSONObject("data"), nodeMap);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
public boolean hasSaveLegacyFields(JSONObject data) {
|
||||
return hasAnyField(data, "tableId", "sourceId", "catalogId", "versionId");
|
||||
}
|
||||
|
||||
public DatasetRef readDatasetRef(JSONObject data) {
|
||||
return data == null ? null : data.getObject("datasetRef", DatasetRef.class);
|
||||
}
|
||||
|
||||
public DatasetRef requireSearchDatasetRef(JSONObject data) {
|
||||
DatasetRef datasetRef = readDatasetRef(data);
|
||||
if (datasetRef == null || datasetRef.getSourceId() == null) {
|
||||
throw new BusinessException(SEARCH_SOURCE_MISSING_MESSAGE);
|
||||
}
|
||||
String querySql = data == null ? null : trimToNull(data.getString("querySql"));
|
||||
if (!StringUtils.hasText(querySql)) {
|
||||
throw new BusinessException(SEARCH_SQL_MISSING_MESSAGE);
|
||||
}
|
||||
return datasetRef;
|
||||
}
|
||||
|
||||
public DatasetRef requireSaveDatasetRef(JSONObject data) {
|
||||
if (hasSaveLegacyFields(data)) {
|
||||
throw new BusinessException(SAVE_EXPIRED_MESSAGE);
|
||||
}
|
||||
DatasetRef datasetRef = readDatasetRef(data);
|
||||
if (datasetRef == null || datasetRef.getTableId() == null) {
|
||||
throw new BusinessException(SAVE_EXPIRED_MESSAGE);
|
||||
}
|
||||
return datasetRef;
|
||||
}
|
||||
|
||||
public DatacenterSchemaResponse loadSchema(DatasetRef datasetRef, Map<BigInteger, DatacenterSchemaResponse> schemaCache) {
|
||||
if (datasetRef == null || datasetRef.getTableId() == null) {
|
||||
throw new BusinessException("缺少已接入表配置");
|
||||
}
|
||||
BigInteger tableId = datasetRef.getTableId();
|
||||
if (schemaCache != null && schemaCache.containsKey(tableId)) {
|
||||
return schemaCache.get(tableId);
|
||||
}
|
||||
DatacenterSchemaResponse schema = queryService.getSchema(copyDatasetRef(datasetRef));
|
||||
if (schemaCache != null) {
|
||||
schemaCache.put(tableId, schema);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
public JSONObject buildSourceSummary(DatasetRef datasetRef) {
|
||||
if (datasetRef == null || datasetRef.getSourceId() == null) {
|
||||
throw new BusinessException(SEARCH_SOURCE_MISSING_MESSAGE);
|
||||
}
|
||||
DatacenterSource source = registryService.getSourceRequired(datasetRef.getSourceId());
|
||||
List<DatacenterTable> managedTables = registryService.listManagedTables(datasetRef.getSourceId(), datasetRef.getCatalogId());
|
||||
managedTables.sort(Comparator.comparing(table -> table.getTableName() == null ? "" : table.getTableName()));
|
||||
JSONObject sourceSummary = new JSONObject();
|
||||
sourceSummary.put("sourceName", source.getSourceName());
|
||||
sourceSummary.put("sourceType", source.getSourceType());
|
||||
JSONArray tables = new JSONArray();
|
||||
for (DatacenterTable table : managedTables) {
|
||||
DatacenterTable fullTable = registryService.getTableWithFields(table.getId());
|
||||
DatacenterCatalog catalog = registryService.getCatalogById(fullTable.getCatalogId());
|
||||
if (StringUtils.hasText(datasetRef.getCatalogName())
|
||||
&& (catalog == null || !datasetRef.getCatalogName().equals(catalog.getCatalogName()))) {
|
||||
continue;
|
||||
}
|
||||
JSONObject tableSummary = new JSONObject();
|
||||
tableSummary.put("catalogName", catalog == null ? null : catalog.getCatalogName());
|
||||
tableSummary.put("tableName", fullTable.getTableName());
|
||||
tableSummary.put("tableDesc", fullTable.getTableDesc());
|
||||
JSONArray fields = new JSONArray();
|
||||
if (fullTable.getFields() != null) {
|
||||
for (DatacenterTableField field : fullTable.getFields()) {
|
||||
JSONObject fieldSummary = new JSONObject();
|
||||
fieldSummary.put("fieldName", field.getFieldName());
|
||||
fieldSummary.put("fieldDesc", field.getFieldDesc());
|
||||
fieldSummary.put("fieldType", resolveFieldType(field));
|
||||
fields.add(fieldSummary);
|
||||
}
|
||||
}
|
||||
tableSummary.put("fields", fields);
|
||||
tables.add(tableSummary);
|
||||
}
|
||||
sourceSummary.put("tables", tables);
|
||||
return sourceSummary;
|
||||
}
|
||||
|
||||
private void injectQueryDataContext(JSONObject data, Map<String, JSONObject> nodeMap) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray nodeIds = data.getJSONArray("queryContextNodeIds");
|
||||
if (nodeIds == null || nodeIds.isEmpty()) {
|
||||
removeQueryDataContextParameter(data);
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, JSONObject> sourceSummaries = new LinkedHashMap<>();
|
||||
Set<String> visitedNodeIds = new LinkedHashSet<>();
|
||||
for (int i = 0; i < nodeIds.size(); i++) {
|
||||
String nodeId = trimToNull(nodeIds.getString(i));
|
||||
if (!StringUtils.hasText(nodeId) || !visitedNodeIds.add(nodeId)) {
|
||||
continue;
|
||||
}
|
||||
JSONObject targetNode = nodeMap.get(nodeId);
|
||||
if (targetNode == null || !isSearchDatasetNode(targetNode.getString("type"))) {
|
||||
throw new BusinessException(INVALID_QUERY_CONTEXT_MESSAGE);
|
||||
}
|
||||
DatasetRef datasetRef = requireSearchDatasetRef(targetNode.getJSONObject("data"));
|
||||
sourceSummaries.putIfAbsent(datasetRef.getSourceId(), buildSourceSummary(datasetRef));
|
||||
}
|
||||
String contextValue = QUERY_CONTEXT_PROMPT + "\n" + JSON.toJSONString(new ArrayList<>(sourceSummaries.values()));
|
||||
upsertQueryDataContextParameter(data, contextValue);
|
||||
}
|
||||
|
||||
private String resolveFieldType(DatacenterTableField field) {
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.hasText(field.getJdbcType())) {
|
||||
return field.getJdbcType();
|
||||
}
|
||||
try {
|
||||
EnumFieldType enumFieldType = EnumFieldType.getByCode(field.getFieldType());
|
||||
if (enumFieldType != null) {
|
||||
return enumFieldType.getText();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return field.getFieldType() == null ? null : String.valueOf(field.getFieldType());
|
||||
}
|
||||
|
||||
private void upsertQueryDataContextParameter(JSONObject data, String contextValue) {
|
||||
JSONArray parameters = data.getJSONArray("parameters");
|
||||
if (parameters == null) {
|
||||
parameters = new JSONArray();
|
||||
data.put("parameters", parameters);
|
||||
}
|
||||
for (int i = parameters.size() - 1; i >= 0; i--) {
|
||||
JSONObject parameter = parameters.getJSONObject(i);
|
||||
if (parameter != null && QUERY_DATA_CONTEXT.equals(parameter.getString("name"))) {
|
||||
parameters.remove(i);
|
||||
}
|
||||
}
|
||||
JSONObject parameter = new JSONObject();
|
||||
parameter.put("id", QUERY_DATA_CONTEXT);
|
||||
parameter.put("name", QUERY_DATA_CONTEXT);
|
||||
parameter.put("title", "查询上下文");
|
||||
parameter.put("description", "数据查询规则与连接表摘要");
|
||||
parameter.put("dataType", "String");
|
||||
parameter.put("refType", "fixed");
|
||||
parameter.put("value", contextValue);
|
||||
parameter.put("required", false);
|
||||
parameter.put("nameDisabled", true);
|
||||
parameter.put("dataTypeDisabled", true);
|
||||
parameter.put("deleteDisabled", true);
|
||||
parameters.add(parameter);
|
||||
}
|
||||
|
||||
private void removeQueryDataContextParameter(JSONObject data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
JSONArray parameters = data.getJSONArray("parameters");
|
||||
if (parameters == null || parameters.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = parameters.size() - 1; i >= 0; i--) {
|
||||
JSONObject parameter = parameters.getJSONObject(i);
|
||||
if (parameter != null && QUERY_DATA_CONTEXT.equals(parameter.getString("name"))) {
|
||||
parameters.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, JSONObject> buildNodeMap(JSONArray nodes) {
|
||||
Map<String, JSONObject> nodeMap = new LinkedHashMap<>();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
JSONObject node = nodes.getJSONObject(i);
|
||||
if (node == null) {
|
||||
continue;
|
||||
}
|
||||
String nodeId = trimToNull(node.getString("id"));
|
||||
if (StringUtils.hasText(nodeId)) {
|
||||
nodeMap.put(nodeId, node);
|
||||
}
|
||||
}
|
||||
return nodeMap;
|
||||
}
|
||||
|
||||
private DatasetRef copyDatasetRef(DatasetRef datasetRef) {
|
||||
DatasetRef copy = new DatasetRef();
|
||||
copy.setSourceId(datasetRef.getSourceId());
|
||||
copy.setCatalogId(datasetRef.getCatalogId());
|
||||
copy.setCatalogName(datasetRef.getCatalogName());
|
||||
copy.setTableId(datasetRef.getTableId());
|
||||
copy.setTableName(datasetRef.getTableName());
|
||||
copy.setVersionId(datasetRef.getVersionId());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private boolean hasAnyField(JSONObject data, String... fieldNames) {
|
||||
if (data == null || fieldNames == null) {
|
||||
return false;
|
||||
}
|
||||
for (String fieldName : fieldNames) {
|
||||
if (data.containsKey(fieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private String shortError(Throwable throwable) {
|
||||
if (throwable == null) {
|
||||
return "unknown";
|
||||
}
|
||||
String message = throwable.getMessage();
|
||||
if (!StringUtils.hasText(message)) {
|
||||
return throwable.getClass().getSimpleName();
|
||||
}
|
||||
return message.length() > 120 ? message.substring(0, 120) + "..." : message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
|
||||
import tech.easyflow.datacenter.execution.service.DatacenterDatasetWriteService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SaveDatasetNode extends BaseNode {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SaveDatasetNode.class);
|
||||
|
||||
private DatasetRef datasetRef;
|
||||
|
||||
public SaveDatasetNode() {
|
||||
}
|
||||
|
||||
public SaveDatasetNode(DatasetRef datasetRef) {
|
||||
this.datasetRef = datasetRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> state = chain.getState().resolveParameters(this);
|
||||
JSONObject payload = new JSONObject(state);
|
||||
JSONArray saveList = payload.getJSONArray("saveList");
|
||||
if (saveList == null || saveList.isEmpty()) {
|
||||
throw new RuntimeException("saveList 不能为空");
|
||||
}
|
||||
LoginAccount account = WorkFlowUtil.getOperator(chain);
|
||||
DatacenterDatasetWriteService writeService = SpringContextUtil.getBean(DatacenterDatasetWriteService.class);
|
||||
DatacenterDatasetQueryService queryService = SpringContextUtil.getBean(DatacenterDatasetQueryService.class);
|
||||
int successRows = 0;
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
for (Object item : saveList) {
|
||||
JSONObject row = item instanceof JSONObject json ? json : JSONObject.from(item);
|
||||
writeService.saveRow(datasetRef, row, account);
|
||||
successRows++;
|
||||
}
|
||||
var schema = queryService.getSchema(datasetRef);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("successRows", successRows);
|
||||
result.put("source", schema.getSource());
|
||||
result.put("catalog", schema.getCatalog());
|
||||
result.put("table", schema.getTable());
|
||||
result.put("version", datasetRef.getVersionId());
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error("工作流保存数据到统一数据集失败,datasetRef={}", datasetRef, ex);
|
||||
throw ex;
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
}
|
||||
|
||||
public DatasetRef getDatasetRef() {
|
||||
return datasetRef;
|
||||
}
|
||||
|
||||
public void setDatasetRef(DatasetRef datasetRef) {
|
||||
this.datasetRef = datasetRef;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
|
||||
public class SaveDatasetNodeParser extends BaseNodeParser {
|
||||
|
||||
private static final String EXPIRED_MESSAGE = "写入数据节点配置已过期,请重新选择已接入表";
|
||||
|
||||
@Override
|
||||
protected BaseNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) {
|
||||
if (data == null) {
|
||||
throw new BusinessException(EXPIRED_MESSAGE);
|
||||
}
|
||||
if (hasLegacyFields(data)) {
|
||||
throw new BusinessException(EXPIRED_MESSAGE);
|
||||
}
|
||||
DatasetRef datasetRef = data.getObject("datasetRef", DatasetRef.class);
|
||||
if (datasetRef == null || datasetRef.getTableId() == null) {
|
||||
throw new BusinessException(EXPIRED_MESSAGE);
|
||||
}
|
||||
return new SaveDatasetNode(datasetRef);
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return "save-dataset-node";
|
||||
}
|
||||
|
||||
private boolean hasLegacyFields(JSONObject data) {
|
||||
if (data == null) {
|
||||
return false;
|
||||
}
|
||||
return data.containsKey("tableId")
|
||||
|| data.containsKey("sourceId")
|
||||
|| data.containsKey("catalogId")
|
||||
|| data.containsKey("versionId");
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.datacenter.service.DatacenterTableService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SaveToDatacenterNode extends BaseNode {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SaveToDatacenterNode.class);
|
||||
|
||||
private BigInteger tableId;
|
||||
|
||||
public SaveToDatacenterNode() {
|
||||
}
|
||||
|
||||
public SaveToDatacenterNode(BigInteger tableId) {
|
||||
this.tableId = tableId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
|
||||
Map<String, Object> map = chain.getState().resolveParameters(this);
|
||||
JSONObject json = new JSONObject(map);
|
||||
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
|
||||
// 默认为未知来源
|
||||
LoginAccount account = WorkFlowUtil.getOperator(chain);
|
||||
|
||||
DatacenterTableService service = SpringContextUtil.getBean(DatacenterTableService.class);
|
||||
|
||||
JSONArray saveList = json.getJSONArray("saveList");
|
||||
|
||||
int successRows = 0;
|
||||
for (Object object : saveList) {
|
||||
JSONObject obj = new JSONObject((com.alibaba.fastjson.JSONObject) object);
|
||||
obj.put("table_id", tableId);
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
service.saveValue(tableId, obj, account);
|
||||
} catch (Exception e) {
|
||||
log.error("工作流保存数据到数据中枢失败,表ID:{},具体值:{}", tableId, obj, e);
|
||||
throw e;
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
successRows++;
|
||||
}
|
||||
|
||||
res.put("successRows", successRows);
|
||||
return res;
|
||||
}
|
||||
|
||||
public BigInteger getTableId() {
|
||||
return tableId;
|
||||
}
|
||||
|
||||
public void setTableId(BigInteger tableId) {
|
||||
this.tableId = tableId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class SaveToDatacenterNodeParser extends BaseNodeParser {
|
||||
|
||||
@Override
|
||||
protected BaseNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) {
|
||||
BigInteger tableId = data.getBigInteger("tableId");
|
||||
if (tableId == null) {
|
||||
throw new RuntimeException("请选择数据表");
|
||||
}
|
||||
return new SaveToDatacenterNode(tableId);
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return "save-to-datacenter-node";
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.easyagents.core.util.StringUtil;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.row.Row;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tech.easyflow.common.entity.DatacenterQuery;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.datacenter.entity.DatacenterTableField;
|
||||
import tech.easyflow.datacenter.service.DatacenterTableService;
|
||||
import tech.easyflow.datacenter.utils.WhereConditionSecurityChecker;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SearchDatacenterNode extends BaseNode {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchDatacenterNode.class);
|
||||
private BigInteger tableId;
|
||||
private String where;
|
||||
private Long limit;
|
||||
|
||||
public SearchDatacenterNode() {
|
||||
}
|
||||
|
||||
public SearchDatacenterNode(BigInteger tableId, String where, Long limit) {
|
||||
this.tableId = tableId;
|
||||
this.where = where;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
|
||||
Map<String, Object> map = chain.getState().resolveParameters(this);
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
long limitNum = 10;
|
||||
if (limit != null) {
|
||||
limitNum = Long.parseLong(limit.toString());
|
||||
}
|
||||
|
||||
DatacenterTableService service = SpringContextUtil.getBean(DatacenterTableService.class);
|
||||
|
||||
DatacenterQuery condition = new DatacenterQuery();
|
||||
condition.setTableId(tableId);
|
||||
condition.setPageNumber(1L);
|
||||
condition.setPageSize(limitNum);
|
||||
// 组合查询条件
|
||||
if (where != null) {
|
||||
setCondition(where, condition, map);
|
||||
}
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
Page<Row> pageData = service.getPageData(condition);
|
||||
|
||||
String key = "rows";
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
if (outputDefs != null && !outputDefs.isEmpty()) {
|
||||
String defName = outputDefs.get(0).getName();
|
||||
if (StringUtil.hasText(defName)) key = defName;
|
||||
}
|
||||
res.put(key, pageData.getRecords());
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public BigInteger getTableId() {
|
||||
return tableId;
|
||||
}
|
||||
|
||||
public void setTableId(BigInteger tableId) {
|
||||
this.tableId = tableId;
|
||||
}
|
||||
|
||||
public String getWhere() {
|
||||
return where;
|
||||
}
|
||||
|
||||
public void setWhere(String where) {
|
||||
this.where = where;
|
||||
}
|
||||
|
||||
public Long getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(Long limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
private void setCondition(String where, DatacenterQuery condition, Map<String, Object> params) {
|
||||
// 条件封装
|
||||
Pattern pattern = Pattern.compile("\\{\\{(.+?)\\}\\}");
|
||||
Matcher matcher = pattern.matcher(where);
|
||||
|
||||
StringBuffer result = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String key = matcher.group(1);
|
||||
Object value = params.get(key);
|
||||
if (value == null) {
|
||||
throw new RuntimeException("参数" + key + "不存在");
|
||||
}
|
||||
String replacement = value.toString();
|
||||
matcher.appendReplacement(result, "'" + replacement + "'");
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
|
||||
try {
|
||||
Expression expression = CCJSqlParserUtil.parseCondExpression(result.toString());
|
||||
if (expression != null) {
|
||||
WhereConditionSecurityChecker checker = new WhereConditionSecurityChecker();
|
||||
DatacenterTableService service = SpringContextUtil.getBean(DatacenterTableService.class);
|
||||
List<DatacenterTableField> fields = service.getFields(tableId);
|
||||
Set<String> columns = fields.stream().map(DatacenterTableField::getFieldName).collect(Collectors.toSet());
|
||||
columns.add("id");
|
||||
columns.add("created");
|
||||
columns.add("modified");
|
||||
columns.add("created_by");
|
||||
columns.add("modified_by");
|
||||
checker.checkConditionSafety(expression, columns);
|
||||
condition.setWhere(expression.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("WHERE SQL解析错误:", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class SearchDatacenterNodeParser extends BaseNodeParser {
|
||||
|
||||
@Override
|
||||
protected BaseNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) {
|
||||
BigInteger tableId = data.getBigInteger("tableId");
|
||||
String where = data.getString("where");
|
||||
Long limit = data.getLong("limit");
|
||||
if (tableId == null) {
|
||||
throw new RuntimeException("请选择数据表");
|
||||
}
|
||||
return new SearchDatacenterNode(tableId,where,limit);
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return "search-datacenter-node";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.easyagents.core.util.StringUtil;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.mybatisflex.core.row.Row;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.model.DatacenterSqlQueryRequest;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
import tech.easyflow.datacenter.execution.service.DatacenterDatasetQueryService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SearchDatasetNode extends BaseNode {
|
||||
|
||||
private static final Pattern PARAM_PATTERN = Pattern.compile("\\{\\{(.+?)\\}\\}");
|
||||
|
||||
private DatasetRef datasetRef;
|
||||
private String querySql;
|
||||
|
||||
public SearchDatasetNode() {
|
||||
}
|
||||
|
||||
public SearchDatasetNode(DatasetRef datasetRef) {
|
||||
this.datasetRef = datasetRef;
|
||||
}
|
||||
|
||||
public SearchDatasetNode(DatasetRef datasetRef, String querySql) {
|
||||
this.datasetRef = datasetRef;
|
||||
this.querySql = querySql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
Map<String, Object> params = chain.getState().resolveParameters(this);
|
||||
DatacenterDatasetQueryService queryService = SpringContextUtil.getBean(DatacenterDatasetQueryService.class);
|
||||
DatacenterSqlQueryRequest request = buildRuntimeRequest(params);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
List<Row> rows = queryService.queryBySql(request);
|
||||
result.put(resolveOutputKey("data"), rows);
|
||||
return result;
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
}
|
||||
|
||||
private DatacenterSqlQueryRequest buildRuntimeRequest(Map<String, Object> params) {
|
||||
DatacenterSqlQueryRequest request = new DatacenterSqlQueryRequest();
|
||||
request.setDatasetRef(copyDatasetRef());
|
||||
request.setSql(resolveQuerySql(params));
|
||||
return request;
|
||||
}
|
||||
|
||||
private String resolveQuerySql(Map<String, Object> params) {
|
||||
String sql = resolveTemplateString(querySql, params);
|
||||
if (!StringUtil.hasText(sql)) {
|
||||
throw new BusinessException("查询数据节点未设置 SQL");
|
||||
}
|
||||
return sql.trim();
|
||||
}
|
||||
|
||||
private DatasetRef copyDatasetRef() {
|
||||
DatasetRef copy = new DatasetRef();
|
||||
copy.setSourceId(datasetRef == null ? null : datasetRef.getSourceId());
|
||||
copy.setCatalogId(datasetRef == null ? null : datasetRef.getCatalogId());
|
||||
copy.setCatalogName(datasetRef == null ? null : datasetRef.getCatalogName());
|
||||
copy.setTableId(null);
|
||||
copy.setTableName(null);
|
||||
copy.setVersionId(null);
|
||||
return copy;
|
||||
}
|
||||
|
||||
private String resolveTemplateString(String text, Map<String, Object> params) {
|
||||
if (!StringUtil.hasText(text) || params == null || params.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
Matcher matcher = PARAM_PATTERN.matcher(text);
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
Object replacement = params.get(matcher.group(1));
|
||||
matcher.appendReplacement(buffer, replacement == null ? "" : Matcher.quoteReplacement(String.valueOf(replacement)));
|
||||
}
|
||||
matcher.appendTail(buffer);
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
private String resolveOutputKey(String defaultName) {
|
||||
List<Parameter> outputDefs = getOutputDefs();
|
||||
if (outputDefs == null || outputDefs.isEmpty()) {
|
||||
return defaultName;
|
||||
}
|
||||
String name = outputDefs.get(0).getName();
|
||||
return StringUtil.hasText(name) ? name : defaultName;
|
||||
}
|
||||
|
||||
public DatasetRef getDatasetRef() {
|
||||
return datasetRef;
|
||||
}
|
||||
|
||||
public void setDatasetRef(DatasetRef datasetRef) {
|
||||
this.datasetRef = datasetRef;
|
||||
}
|
||||
|
||||
public String getQuerySql() {
|
||||
return querySql;
|
||||
}
|
||||
|
||||
public void setQuerySql(String querySql) {
|
||||
this.querySql = querySql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
|
||||
public class SearchDatasetNodeParser extends BaseNodeParser {
|
||||
|
||||
private static final String SOURCE_MISSING_MESSAGE = "查询数据节点未选择连接服务";
|
||||
private static final String SQL_MISSING_MESSAGE = "查询数据节点未设置 SQL";
|
||||
|
||||
@Override
|
||||
protected BaseNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) {
|
||||
if (data == null) {
|
||||
throw new BusinessException(SOURCE_MISSING_MESSAGE);
|
||||
}
|
||||
DatasetRef datasetRef = data.getObject("datasetRef", DatasetRef.class);
|
||||
if (datasetRef == null || datasetRef.getSourceId() == null) {
|
||||
throw new BusinessException(SOURCE_MISSING_MESSAGE);
|
||||
}
|
||||
String querySql = data.getString("querySql");
|
||||
if (querySql == null || querySql.isBlank()) {
|
||||
throw new BusinessException(SQL_MISSING_MESSAGE);
|
||||
}
|
||||
return new SearchDatasetNode(datasetRef, querySql);
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return "search-dataset-node";
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.mybatisflex.core.row.Db;
|
||||
import com.mybatisflex.core.row.Row;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import net.sf.jsqlparser.JSQLParserException;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import net.sf.jsqlparser.statement.Statement;
|
||||
import net.sf.jsqlparser.statement.select.Select;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* SQL查询节点
|
||||
*
|
||||
* @author tao
|
||||
* @date 2025-05-21
|
||||
*/
|
||||
public class SqlNode extends BaseNode {
|
||||
|
||||
private String sql;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SqlNode.class);
|
||||
|
||||
public SqlNode() {
|
||||
}
|
||||
|
||||
public SqlNode(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
|
||||
Map<String, Object> map = chain.getState().resolveParameters(this);
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
|
||||
|
||||
Map<String, Object> formatSqlMap = formatSql(sql, map);
|
||||
String formatSql = (String) formatSqlMap.get("replacedSql");
|
||||
|
||||
Statement statement = null;
|
||||
try {
|
||||
statement = CCJSqlParserUtil.parse(formatSql);
|
||||
|
||||
} catch (JSQLParserException e) {
|
||||
logger.error("sql 解析报错:", e);
|
||||
throw new BusinessException("SQL解析失败,请确认SQL语法无误");
|
||||
}
|
||||
|
||||
if (!(statement instanceof Select)) {
|
||||
logger.error("sql 解析报错:statement instanceof Select 结果为false");
|
||||
throw new BusinessException("仅支持查询语句!");
|
||||
}
|
||||
|
||||
List<String> paramNames = (List<String>) formatSqlMap.get("paramNames");
|
||||
|
||||
List<Object> paramValues = new ArrayList<>();
|
||||
paramNames.forEach(paramName -> {
|
||||
Object o = map.get(paramName);
|
||||
paramValues.add(o);
|
||||
});
|
||||
|
||||
List<Row> rows = Db.selectListBySql(formatSql, paramValues.toArray());
|
||||
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
res.put("queryData", rows);
|
||||
return res;
|
||||
}
|
||||
|
||||
private Map<String, Object> formatSql(String rawSql, Map<String, Object> paramMap) {
|
||||
|
||||
if (!StringUtils.hasLength(rawSql)) {
|
||||
logger.error("sql解析报错:sql为空");
|
||||
throw new BusinessException("sql 不能为空!");
|
||||
}
|
||||
|
||||
// 匹配 {{?...}} 表示可用占位符的参数
|
||||
Pattern paramPattern = Pattern.compile("\\{\\{\\?([^}]+)}}");
|
||||
|
||||
// 匹配 {{...}} 表示直接替换的参数(非占位符)
|
||||
Pattern directPattern = Pattern.compile("\\{\\{([^}?][^}]*)}}");
|
||||
|
||||
List<String> paramNames = new ArrayList<>();
|
||||
StringBuffer sqlBuffer = new StringBuffer();
|
||||
|
||||
// 替换 {{?...}} -> ?
|
||||
Matcher paramMatcher = paramPattern.matcher(rawSql);
|
||||
while (paramMatcher.find()) {
|
||||
String paramName = paramMatcher.group(1).trim();
|
||||
paramNames.add(paramName);
|
||||
paramMatcher.appendReplacement(sqlBuffer, "?");
|
||||
}
|
||||
paramMatcher.appendTail(sqlBuffer);
|
||||
String intermediateSql = sqlBuffer.toString();
|
||||
|
||||
// 替换 {{...}} -> 实际值(用于表名/列名等)
|
||||
sqlBuffer = new StringBuffer(); // 清空 buffer 重新处理
|
||||
Matcher directMatcher = directPattern.matcher(intermediateSql);
|
||||
while (directMatcher.find()) {
|
||||
String key = directMatcher.group(1).trim();
|
||||
Object value = paramMap.get(key);
|
||||
if (value == null) {
|
||||
logger.error("未找到参数:" + key);
|
||||
throw new BusinessException("sql解析失败,请确保sql语法正确!");
|
||||
}
|
||||
|
||||
String safeValue = value.toString();
|
||||
|
||||
directMatcher.appendReplacement(sqlBuffer, Matcher.quoteReplacement(safeValue));
|
||||
}
|
||||
directMatcher.appendTail(sqlBuffer);
|
||||
|
||||
String finalSql = sqlBuffer.toString().trim();
|
||||
|
||||
// 清理末尾分号与中文引号
|
||||
if (finalSql.endsWith(";") || finalSql.endsWith(";")) {
|
||||
finalSql = finalSql.substring(0, finalSql.length() - 1);
|
||||
}
|
||||
finalSql = finalSql.replace("“", "\"").replace("”", "\"");
|
||||
|
||||
logger.info("Final SQL: {}", finalSql);
|
||||
logger.info("Param names: {}", paramNames);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("replacedSql", finalSql);
|
||||
result.put("paramNames", paramNames);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public void setSql(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
|
||||
/**
|
||||
* Sql查询节点解析
|
||||
*
|
||||
* @author tao
|
||||
* @date 2025-05-21
|
||||
*/
|
||||
public class SqlNodeParser extends BaseNodeParser {
|
||||
|
||||
|
||||
@Override
|
||||
public BaseNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) {
|
||||
String sql = data.getString("sql");
|
||||
return new SqlNode(sql);
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return "sql-node";
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.junit.Test;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.node.SearchDatasetNodeParser;
|
||||
import tech.easyflow.ai.node.WorkflowNodeParser;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
|
||||
@@ -69,6 +70,24 @@ public class WorkflowCheckServiceTest {
|
||||
assertHasCode(result, "EDGE_TARGET_NOT_FOUND");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveShouldBlockSearchDatasetWithoutSql() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
JSONObject searchData = data("查询数据");
|
||||
JSONObject datasetRef = new JSONObject();
|
||||
datasetRef.put("sourceId", "1001");
|
||||
datasetRef.put("tableId", "2001");
|
||||
searchData.put("datasetRef", datasetRef);
|
||||
String content = workflowJson(
|
||||
array(node("search-1", "search-dataset-node", null, searchData)),
|
||||
new JSONArray()
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
|
||||
Assert.assertFalse(result.isPassed());
|
||||
assertHasCode(result, "SEARCH_DATASET_INVALID");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockMissingStartOrEnd() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
@@ -86,6 +105,26 @@ public class WorkflowCheckServiceTest {
|
||||
assertHasCode(result, "END_NODE_MISSING");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldPassForSourceOnlySearchDatasetNode() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
String content = workflowJson(
|
||||
array(
|
||||
node("s1", "startNode", null, data("开始")),
|
||||
searchDatasetNode("q1", null, "1001"),
|
||||
node("e1", "endNode", null, data("结束"))
|
||||
),
|
||||
array(
|
||||
edge("e1", "s1", "q1"),
|
||||
edge("e2", "q1", "e1")
|
||||
)
|
||||
);
|
||||
|
||||
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
|
||||
Assert.assertTrue(result.isPassed());
|
||||
Assert.assertEquals(0, result.getIssueCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreExecuteShouldBlockRootEntryNotStart() throws Exception {
|
||||
WorkflowCheckService service = newService(new HashMap<>());
|
||||
@@ -203,8 +242,10 @@ public class WorkflowCheckServiceTest {
|
||||
.withDefaultParsers(true)
|
||||
.build();
|
||||
parser.addNodeParser("workflow-node", new WorkflowNodeParser());
|
||||
parser.addNodeParser("search-dataset-node", new SearchDatasetNodeParser());
|
||||
setField(service, "chainParser", parser);
|
||||
setField(service, "workflowService", mockWorkflowService(workflowStore));
|
||||
setField(service, "workflowDatacenterContentService", new WorkflowDatacenterContentService());
|
||||
return service;
|
||||
}
|
||||
|
||||
@@ -294,6 +335,15 @@ public class WorkflowCheckServiceTest {
|
||||
return node(id, "workflow-node", parentId, data);
|
||||
}
|
||||
|
||||
private static JSONObject searchDatasetNode(String id, String parentId, String sourceId) {
|
||||
JSONObject data = data("查询数据");
|
||||
JSONObject datasetRef = new JSONObject();
|
||||
datasetRef.put("sourceId", sourceId);
|
||||
data.put("datasetRef", datasetRef);
|
||||
data.put("querySql", "SELECT 1");
|
||||
return node(id, "search-dataset-node", parentId, data);
|
||||
}
|
||||
|
||||
private static JSONObject data(String title) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("title", title);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.datacenter.execution.model.DatasetRef;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SearchDatasetNodeTest {
|
||||
|
||||
@Test
|
||||
public void testResolveQuerySqlShouldUseNodeQuerySqlAndResolveTemplate() throws Exception {
|
||||
DatasetRef datasetRef = new DatasetRef();
|
||||
datasetRef.setSourceId(BigInteger.valueOf(1001L));
|
||||
SearchDatasetNode node = new SearchDatasetNode(datasetRef, """
|
||||
SELECT id, name
|
||||
FROM orders_{{biz}}
|
||||
WHERE name LIKE '%{{keyword}}%'
|
||||
ORDER BY created_at {{direction}}
|
||||
""");
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("biz", "prod");
|
||||
params.put("keyword", "vip");
|
||||
params.put("direction", "DESC");
|
||||
|
||||
String sql = invokeResolveQuerySql(node, params);
|
||||
Assert.assertEquals("""
|
||||
SELECT id, name
|
||||
FROM orders_prod
|
||||
WHERE name LIKE '%vip%'
|
||||
ORDER BY created_at DESC
|
||||
""".trim(), sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveQuerySqlShouldUseNodeQuerySqlWhenParamsDoNotContainSql() throws Exception {
|
||||
DatasetRef datasetRef = new DatasetRef();
|
||||
datasetRef.setSourceId(BigInteger.valueOf(2002L));
|
||||
SearchDatasetNode node = new SearchDatasetNode(datasetRef, "SELECT * FROM fallback_table");
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
|
||||
String sql = invokeResolveQuerySql(node, params);
|
||||
Assert.assertEquals("SELECT * FROM fallback_table", sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveQuerySqlShouldRejectBlankSql() throws Exception {
|
||||
DatasetRef datasetRef = new DatasetRef();
|
||||
datasetRef.setSourceId(BigInteger.valueOf(3003L));
|
||||
SearchDatasetNode node = new SearchDatasetNode(datasetRef, " ");
|
||||
|
||||
try {
|
||||
invokeResolveQuerySql(node, new HashMap<>());
|
||||
Assert.fail("expected BusinessException");
|
||||
} catch (Exception e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
Assert.assertTrue(cause instanceof BusinessException);
|
||||
Assert.assertEquals("查询数据节点未设置 SQL", cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String invokeResolveQuerySql(SearchDatasetNode node, Map<String, Object> params) throws Exception {
|
||||
Method method = SearchDatasetNode.class.getDeclaredMethod("resolveQuerySql", Map.class);
|
||||
method.setAccessible(true);
|
||||
return (String) method.invoke(node, params);
|
||||
}
|
||||
}
|
||||
@@ -43,11 +43,19 @@
|
||||
"fileDownloadURL": "FileDownloadURL",
|
||||
"pluginSelect": "PluginSelect",
|
||||
"saveData": "SaveData",
|
||||
"saveDataset": "Write Data",
|
||||
"dataToBeSaved": "DataToBeSaved",
|
||||
"successInsertedRecords": "SuccessInsertedRecords",
|
||||
"dataTable": "DataTable",
|
||||
"dataset": "Dataset",
|
||||
"datasetDsl": "Query Conditions",
|
||||
"datasetDslPlaceholder": "Structured query conditions",
|
||||
"queryData": "QueryData",
|
||||
"queryDataset": "Query Data",
|
||||
"querySpec": "SQL",
|
||||
"querySql": "SQL",
|
||||
"queryResult": "QueryResult",
|
||||
"querySummary": "QuerySummary",
|
||||
"filterConditions": "FilterConditions",
|
||||
"limit": "Limit",
|
||||
"sqlQuery": "SQL Query",
|
||||
@@ -79,9 +87,15 @@
|
||||
"fileDownloadURL": "Generated file URL",
|
||||
"plugin": "Select a predefined plugin",
|
||||
"saveData": "Save data to data hub",
|
||||
"saveDataset": "Write data into a managed table",
|
||||
"dataToBeSaved": "List of data to be saved",
|
||||
"dataTable": "Please select a data table",
|
||||
"dataset": "Please select a managed table",
|
||||
"queryData": "Query data from the data hub",
|
||||
"queryDataset": "Run a read-only SQL query through the selected connection service",
|
||||
"querySpec": "Enter SQL. Parameters can be referenced",
|
||||
"querySql": "Enter SQL. Parameters can be referenced",
|
||||
"datasetDsl": "Enter read-only SQL. Write statements, multiple statements, and unmanaged tables are not allowed",
|
||||
"sqlQuery": "Query the database via SQL",
|
||||
"enterSQL": "Please enter the SQL statement",
|
||||
"queryResultJson": "Query result (JSON object)",
|
||||
|
||||
@@ -43,11 +43,19 @@
|
||||
"fileDownloadURL": "文件下载地址",
|
||||
"pluginSelect": "插件选择",
|
||||
"saveData": "保存数据",
|
||||
"saveDataset": "写入数据",
|
||||
"dataToBeSaved": "待保存的数据",
|
||||
"successInsertedRecords": "成功插入条数",
|
||||
"dataTable": "数据表",
|
||||
"dataset": "数据集",
|
||||
"datasetDsl": "查询条件",
|
||||
"datasetDslPlaceholder": "结构化查询条件",
|
||||
"queryData": "查询数据",
|
||||
"queryDataset": "查询数据",
|
||||
"querySpec": "SQL",
|
||||
"querySql": "SQL",
|
||||
"queryResult": "查询结果",
|
||||
"querySummary": "查询摘要",
|
||||
"filterConditions": "过滤条件",
|
||||
"limit": "限制条数",
|
||||
"sqlQuery": "SQL 查询",
|
||||
@@ -79,9 +87,15 @@
|
||||
"fileDownloadURL": "生成后的文件地址",
|
||||
"plugin": "选择定义好的插件",
|
||||
"saveData": "保存数据到数据中枢",
|
||||
"saveDataset": "将数据写入已接入表",
|
||||
"dataToBeSaved": "待保存的数据列表",
|
||||
"dataTable": "请选择数据表",
|
||||
"dataset": "请选择已接入表",
|
||||
"queryData": "查询数据中枢的数据",
|
||||
"queryDataset": "按连接服务执行只读 SQL 查询",
|
||||
"querySpec": "请输入 SQL,可引用输入参数",
|
||||
"querySql": "请输入 SQL,可引用输入参数",
|
||||
"datasetDsl": "请输入只读 SQL,不支持写入语句、多语句和未接入表访问",
|
||||
"sqlQuery": "通过 SQL 查询数据库",
|
||||
"enterSQL": "请输入SQL语句",
|
||||
"queryResultJson": "查询结果(json对象)",
|
||||
|
||||
@@ -6,7 +6,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ant-design:apartment-outlined',
|
||||
title: $t('datacenterTable.title'),
|
||||
title: '工作流设计',
|
||||
hideInMenu: true,
|
||||
activePath: '/ai/workflow',
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
|
||||
import WorkflowSteps from '#/views/ai/workflow/components/WorkflowSteps.vue';
|
||||
|
||||
import {getCustomNode} from './customNode/index';
|
||||
import nodeNames from './customNode/nodeNames';
|
||||
|
||||
import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
@@ -59,17 +58,35 @@ const codeEngineList = ref<any[]>([
|
||||
available: true,
|
||||
},
|
||||
]);
|
||||
|
||||
function escapeHtmlAttr(value?: string) {
|
||||
return String(value || '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>');
|
||||
}
|
||||
|
||||
function buildModelIconMarkup(icon?: string, providerType?: string) {
|
||||
const normalized = String(icon || '').trim();
|
||||
if (normalized) {
|
||||
if (normalized.startsWith('<svg') || normalized.startsWith('<img')) {
|
||||
return normalized;
|
||||
}
|
||||
return `<img src="${escapeHtmlAttr(normalized)}" alt="" style="width:100%; height:100%; object-fit:contain;" />`;
|
||||
}
|
||||
if (!providerType) {
|
||||
return undefined;
|
||||
}
|
||||
return getIconByValue(providerType) || undefined;
|
||||
}
|
||||
|
||||
const provider = computed(() => ({
|
||||
llm: () => llmList.value.map((item: any) => {
|
||||
let iconStr = undefined;
|
||||
if (item.modelProvider?.icon) {
|
||||
iconStr = `<img src="${item.modelProvider.icon}" style="width:100%; height:100%; object-fit:contain;" />`;
|
||||
} else if (item.modelProvider?.providerType) {
|
||||
const svgStr = getIconByValue(item.modelProvider.providerType);
|
||||
if (svgStr) {
|
||||
iconStr = svgStr;
|
||||
}
|
||||
}
|
||||
const iconStr = buildModelIconMarkup(
|
||||
item.modelProvider?.icon,
|
||||
item.modelProvider?.providerType,
|
||||
);
|
||||
|
||||
// Extract brand and model name directly from the title if it contains '/'
|
||||
let displayTitle = item.title || '';
|
||||
@@ -330,15 +347,15 @@ async function runCheck(stage: WorkflowCheckStage, silentPass: boolean = false)
|
||||
stage,
|
||||
});
|
||||
checkResult.value = res.data;
|
||||
const issues = Array.isArray(res.data?.issues) ? res.data.issues : [];
|
||||
checkIssuesVisible.value = issues.length > 0;
|
||||
if (!res.data?.passed) {
|
||||
checkIssuesVisible.value = true;
|
||||
ElMessage.error($t('aiWorkflow.checkFailed'));
|
||||
return false;
|
||||
}
|
||||
checkIssuesVisible.value = false;
|
||||
focusedIssueKey.value = '';
|
||||
issueFocusActive.value = false;
|
||||
if (!silentPass) {
|
||||
if (!silentPass && issues.length === 0) {
|
||||
ElMessage.success($t('aiWorkflow.checkPassed'));
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
import type {
|
||||
DatasetRefPayload,
|
||||
ManagedDatasetOption,
|
||||
ManagedDatasetSourceOption,
|
||||
} from './datasetOptions';
|
||||
import huaweiIcon from '#/assets/datacenter/huawei-icon.svg';
|
||||
import mysqlIcon from '#/assets/datacenter/mysql-icon.svg';
|
||||
import postgresqlIcon from '#/assets/datacenter/postgresql-icon.svg';
|
||||
import {
|
||||
groupManagedDatasetOptions,
|
||||
loadManagedDatasetOptions,
|
||||
} from './datasetOptions';
|
||||
|
||||
const SOURCE_LOGO_MAP: Record<string, string> = {
|
||||
EXCEL: 'excel',
|
||||
EXCEL_MATERIALIZED: 'excel',
|
||||
GAUSSDB_NATIVE: 'gaussdb',
|
||||
GBASE_8A: 'gbase',
|
||||
GBASE_8S: 'gbase',
|
||||
MYSQL: 'mysql',
|
||||
ORACLE: 'oracle',
|
||||
POSTGRESQL: 'postgresql',
|
||||
PROJECT_MYSQL: 'mysql',
|
||||
};
|
||||
|
||||
type NodeLike = {
|
||||
id: string;
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
|
||||
type UpdateNodeData = (
|
||||
nodeId: string,
|
||||
data: Record<string, any> | ((node: Record<string, any>) => Record<string, any>),
|
||||
) => void;
|
||||
|
||||
type FlowInstance = {
|
||||
updateNodeData: UpdateNodeData;
|
||||
};
|
||||
|
||||
type RenderContext = FlowInstance | undefined;
|
||||
|
||||
type RendererState = {
|
||||
pickerOpen: boolean;
|
||||
loadingOptions: boolean;
|
||||
optionsLoaded: boolean;
|
||||
options: ManagedDatasetOption[];
|
||||
sources: ManagedDatasetSourceOption[];
|
||||
tableSearchText: string;
|
||||
updateNodeData?: UpdateNodeData;
|
||||
};
|
||||
|
||||
function getState(parent: HTMLElement): RendererState {
|
||||
const holder = parent as HTMLElement & { __datasetState?: RendererState };
|
||||
if (!holder.__datasetState) {
|
||||
holder.__datasetState = {
|
||||
pickerOpen: false,
|
||||
loadingOptions: false,
|
||||
optionsLoaded: false,
|
||||
options: [],
|
||||
sources: [],
|
||||
tableSearchText: '',
|
||||
};
|
||||
}
|
||||
return holder.__datasetState;
|
||||
}
|
||||
|
||||
function getUpdateNodeData(parent: HTMLElement, flowInstance?: RenderContext) {
|
||||
const state = getState(parent);
|
||||
if (flowInstance?.updateNodeData) {
|
||||
state.updateNodeData = flowInstance.updateNodeData.bind(flowInstance);
|
||||
}
|
||||
return state.updateNodeData;
|
||||
}
|
||||
|
||||
function getDatasetKey(datasetRef?: DatasetRefPayload | null) {
|
||||
return datasetRef?.tableId == null ? '' : String(datasetRef.tableId);
|
||||
}
|
||||
|
||||
function getSourceKey(datasetRef?: DatasetRefPayload | null) {
|
||||
return datasetRef?.sourceId == null ? '' : String(datasetRef.sourceId);
|
||||
}
|
||||
|
||||
function createSourceOnlyDatasetRef(source: ManagedDatasetSourceOption): DatasetRefPayload {
|
||||
return {
|
||||
sourceId: source.sourceId,
|
||||
catalogId: null,
|
||||
catalogName: '',
|
||||
tableId: null,
|
||||
tableName: '',
|
||||
versionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(value?: string | number | null) {
|
||||
return String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function getSourceLogoType(sourceType?: string) {
|
||||
return SOURCE_LOGO_MAP[sourceType || ''] || 'default';
|
||||
}
|
||||
|
||||
function buildSourceLogo(sourceType?: string) {
|
||||
const logoType = getSourceLogoType(sourceType);
|
||||
if (logoType === 'mysql') {
|
||||
return `<img class="dataset-node-brand-image" src="${mysqlIcon}" alt="" />`;
|
||||
}
|
||||
if (logoType === 'postgresql') {
|
||||
return `<img class="dataset-node-brand-image" src="${postgresqlIcon}" alt="" />`;
|
||||
}
|
||||
if (logoType === 'gaussdb') {
|
||||
return `<img class="dataset-node-brand-image" src="${huaweiIcon}" alt="" />`;
|
||||
}
|
||||
if (logoType === 'oracle') {
|
||||
return `
|
||||
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<rect x="4.6" y="7.2" width="14.8" height="9.6" rx="4.8" fill="#ea4335" />
|
||||
<rect x="7.1" y="9.35" width="9.8" height="5.3" rx="2.65" fill="white" />
|
||||
<rect x="8.4" y="10.65" width="7.2" height="2.7" rx="1.35" fill="#ea4335" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
if (logoType === 'gbase') {
|
||||
return `
|
||||
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="8" fill="#10b981" />
|
||||
<path
|
||||
d="M15.85 9.2C15.1 8.02 13.8 7.3 12.38 7.3C9.94 7.3 8.1 9.14 8.1 11.98C8.1 14.87 10.05 16.7 12.56 16.7C13.88 16.7 15.03 16.18 15.88 15.15V12.55H12.45"
|
||||
stroke="white"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.7"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
if (logoType === 'excel') {
|
||||
return `
|
||||
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<rect x="4.4" y="4.2" width="15.2" height="15.6" rx="3.2" fill="#16a34a" />
|
||||
<path
|
||||
d="M9 9L11.1 12L9 15M15 9L12.9 12L15 15"
|
||||
stroke="white"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.65"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
return `
|
||||
<svg class="dataset-node-brand-svg" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<ellipse cx="12" cy="6.5" rx="5.75" ry="2.5" fill="#94a3b8" />
|
||||
<path d="M6.25 6.5V12.2C6.25 13.58 8.82 14.7 12 14.7C15.18 14.7 17.75 13.58 17.75 12.2V6.5" fill="#cbd5e1" />
|
||||
<path d="M6.25 12.2V17.35C6.25 18.72 8.82 19.85 12 19.85C15.18 19.85 17.75 18.72 17.75 17.35V12.2" fill="#e2e8f0" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildStyles() {
|
||||
return `
|
||||
<style>
|
||||
.dataset-node-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.dataset-node-label {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-muted);
|
||||
}
|
||||
.dataset-node-picker-anchor {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.dataset-node-picker-trigger {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 7px 10px 7px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
background: var(--tf-bg-surface);
|
||||
color: var(--tf-text-primary);
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dataset-node-picker-trigger:hover {
|
||||
border-color: var(--tf-border-color-strong);
|
||||
}
|
||||
.dataset-node-picker-trigger.is-open {
|
||||
border-color: var(--tf-color-primary);
|
||||
}
|
||||
.dataset-node-picker-value {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.dataset-node-picker-placeholder {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
}
|
||||
.dataset-node-picker-text {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.dataset-node-picker-main {
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.dataset-node-picker-arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: var(--tf-text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dataset-node-picker {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--tf-bg-surface);
|
||||
box-shadow: var(--tf-shadow-medium);
|
||||
overflow: hidden;
|
||||
z-index: 40;
|
||||
}
|
||||
.dataset-node-picker-body,
|
||||
.dataset-node-list {
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
.dataset-node-picker-item {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
padding: 9px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
.dataset-node-picker-item:hover {
|
||||
background: var(--tf-bg-muted);
|
||||
}
|
||||
.dataset-node-picker-item.is-active {
|
||||
background: var(--tf-bg-muted);
|
||||
color: var(--tf-color-primary);
|
||||
}
|
||||
.dataset-node-picker-item-inner {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.dataset-node-picker-item-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.dataset-node-picker-item-title {
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.dataset-node-picker-item-meta {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
color: var(--tf-text-secondary);
|
||||
}
|
||||
.dataset-node-empty {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
}
|
||||
.dataset-node-brand {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dataset-node-brand-image,
|
||||
.dataset-node-brand-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
.dataset-node-list-box {
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--tf-bg-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.dataset-node-search {
|
||||
width: 100%;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--tf-text-primary);
|
||||
background: var(--tf-bg-surface);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dataset-node-search:focus {
|
||||
outline: none;
|
||||
border-color: var(--tf-color-primary);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
function filterTableOptions(options: ManagedDatasetOption[], searchText: string) {
|
||||
const keyword = searchText.trim().toLowerCase();
|
||||
if (!keyword) {
|
||||
return options;
|
||||
}
|
||||
return options.filter((option) => option.keywords.includes(keyword));
|
||||
}
|
||||
|
||||
function ensureOptionsLoaded(
|
||||
state: RendererState,
|
||||
parent: HTMLElement,
|
||||
node: NodeLike,
|
||||
flowInstance: RenderContext,
|
||||
rerender: (parent: HTMLElement, node: NodeLike, flowInstance?: RenderContext) => void,
|
||||
) {
|
||||
if (state.loadingOptions || state.optionsLoaded) {
|
||||
return;
|
||||
}
|
||||
state.loadingOptions = true;
|
||||
loadManagedDatasetOptions()
|
||||
.then((options) => {
|
||||
state.options = options;
|
||||
state.sources = groupManagedDatasetOptions(options);
|
||||
state.optionsLoaded = true;
|
||||
})
|
||||
.finally(() => {
|
||||
state.loadingOptions = false;
|
||||
rerender(parent, node, flowInstance);
|
||||
});
|
||||
}
|
||||
|
||||
function buildPickerListItem(
|
||||
title: string,
|
||||
meta: string,
|
||||
active: boolean,
|
||||
action: string,
|
||||
extraAttr: string,
|
||||
iconHtml: string = '',
|
||||
) {
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="dataset-node-picker-item nopan nodrag nowheel ${active ? 'is-active' : ''}"
|
||||
data-action="${action}"
|
||||
${extraAttr}
|
||||
>
|
||||
<span class="dataset-node-picker-item-inner">
|
||||
${iconHtml ? `<span class="dataset-node-brand">${iconHtml}</span>` : ''}
|
||||
<span class="dataset-node-picker-item-main">
|
||||
<span class="dataset-node-picker-item-title">${escapeHtml(title)}</span>
|
||||
${meta ? `<span class="dataset-node-picker-item-meta">${escapeHtml(meta)}</span>` : ''}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildSourceList(options: ManagedDatasetSourceOption[], activeKey: string, emptyText: string) {
|
||||
if (!options.length) {
|
||||
return `<div class="dataset-node-empty">${emptyText}</div>`;
|
||||
}
|
||||
return options
|
||||
.map((option) =>
|
||||
buildPickerListItem(
|
||||
option.sourceName,
|
||||
`${option.tables.length} 张表`,
|
||||
activeKey === String(option.sourceId),
|
||||
'select-source',
|
||||
`data-source-id="${escapeHtml(option.sourceId)}"`,
|
||||
buildSourceLogo(option.sourceType),
|
||||
),
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function buildTableList(options: ManagedDatasetOption[], activeKey: string, emptyText: string) {
|
||||
if (!options.length) {
|
||||
return `<div class="dataset-node-empty">${emptyText}</div>`;
|
||||
}
|
||||
return options
|
||||
.map((option) =>
|
||||
buildPickerListItem(
|
||||
option.tableName,
|
||||
`${option.sourceName} / ${option.catalogName}`,
|
||||
activeKey === String(option.datasetRef.tableId),
|
||||
'select-dataset',
|
||||
`data-table-id="${escapeHtml(option.datasetRef.tableId)}"`,
|
||||
buildSourceLogo(option.sourceType),
|
||||
),
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function buildSearchSummary(currentSource?: ManagedDatasetSourceOption) {
|
||||
if (!currentSource) {
|
||||
return '<span class="dataset-node-picker-placeholder">请选择连接服务</span>';
|
||||
}
|
||||
return `
|
||||
<span class="dataset-node-brand">${buildSourceLogo(currentSource.sourceType)}</span>
|
||||
<span class="dataset-node-picker-text">
|
||||
<span class="dataset-node-picker-main">${escapeHtml(currentSource.sourceName)}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function bindInteractiveElements(parent: HTMLElement) {
|
||||
parent.querySelectorAll<HTMLElement>('button, input, select, textarea').forEach((element) => {
|
||||
element.onpointerdown = (event) => event.stopPropagation();
|
||||
element.onmousedown = (event) => event.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
export function rerenderSearchNode(parent: HTMLElement, node: NodeLike, flowInstance?: RenderContext) {
|
||||
const state = getState(parent);
|
||||
const updateNodeData = getUpdateNodeData(parent, flowInstance);
|
||||
ensureOptionsLoaded(state, parent, node, flowInstance, rerenderSearchNode);
|
||||
|
||||
const datasetRef = (node.data?.datasetRef || null) as DatasetRefPayload | null;
|
||||
const sourceKey = getSourceKey(datasetRef);
|
||||
const currentSource = state.sources.find((item) => String(item.sourceId) === sourceKey);
|
||||
|
||||
parent.innerHTML = `
|
||||
${buildStyles()}
|
||||
<div class="dataset-node-section">
|
||||
<div class="dataset-node-label">连接服务</div>
|
||||
<div class="dataset-node-picker-anchor">
|
||||
<button type="button" class="dataset-node-picker-trigger nopan nodrag nowheel ${state.pickerOpen ? 'is-open' : ''}" data-action="toggle-picker">
|
||||
<span class="dataset-node-picker-value">${buildSearchSummary(currentSource)}</span>
|
||||
<span class="dataset-node-picker-arrow">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg>
|
||||
</span>
|
||||
</button>
|
||||
${state.pickerOpen ? `
|
||||
<div class="dataset-node-picker">
|
||||
<div class="dataset-node-picker-body">
|
||||
${state.loadingOptions ? '<div class="dataset-node-empty">正在加载连接服务...</div>' : buildSourceList(state.sources, sourceKey, '暂无可用连接服务')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
bindInteractiveElements(parent);
|
||||
|
||||
parent.querySelector<HTMLElement>('[data-action="toggle-picker"]')?.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
state.pickerOpen = !state.pickerOpen;
|
||||
rerenderSearchNode(parent, node, flowInstance);
|
||||
});
|
||||
|
||||
parent.querySelectorAll<HTMLElement>('[data-action="select-source"]').forEach((element) => {
|
||||
element.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const sourceId = element.dataset.sourceId;
|
||||
const source = state.sources.find((item) => String(item.sourceId) === String(sourceId));
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
state.pickerOpen = false;
|
||||
const nextDatasetRef = createSourceOnlyDatasetRef(source);
|
||||
node.data = {
|
||||
...(node.data || {}),
|
||||
datasetRef: nextDatasetRef,
|
||||
sourceName: source.sourceName,
|
||||
sourceType: source.sourceType,
|
||||
};
|
||||
updateNodeData?.(node.id, {
|
||||
datasetRef: nextDatasetRef,
|
||||
sourceName: source.sourceName,
|
||||
sourceType: source.sourceType,
|
||||
});
|
||||
rerenderSearchNode(parent, node, flowInstance);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function rerenderSaveNode(parent: HTMLElement, node: NodeLike, flowInstance?: RenderContext) {
|
||||
const state = getState(parent);
|
||||
const updateNodeData = getUpdateNodeData(parent, flowInstance);
|
||||
ensureOptionsLoaded(state, parent, node, flowInstance, rerenderSaveNode);
|
||||
|
||||
const datasetRef = (node.data?.datasetRef || null) as DatasetRefPayload | null;
|
||||
const activeKey = getDatasetKey(datasetRef);
|
||||
const currentOption = state.options.find((item) => String(item.datasetRef.tableId) === activeKey);
|
||||
const filtered = filterTableOptions(state.options, state.tableSearchText);
|
||||
|
||||
parent.innerHTML = `
|
||||
${buildStyles()}
|
||||
<div class="dataset-node-section">
|
||||
<div class="dataset-node-label">已接入表</div>
|
||||
<input class="dataset-node-search nopan nodrag nowheel" data-role="table-search" placeholder="搜索连接 / 库 / 表" value="${escapeHtml(state.tableSearchText)}" />
|
||||
<div class="dataset-node-list-box">
|
||||
<div class="dataset-node-list">
|
||||
${state.loadingOptions ? '<div class="dataset-node-empty">正在加载已接入表...</div>' : buildTableList(filtered, activeKey, '没有匹配的已接入表')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
bindInteractiveElements(parent);
|
||||
|
||||
parent.querySelector<HTMLInputElement>('[data-role="table-search"]')?.addEventListener('input', (event) => {
|
||||
event.stopPropagation();
|
||||
state.tableSearchText = (event.currentTarget as HTMLInputElement).value || '';
|
||||
rerenderSaveNode(parent, node, flowInstance);
|
||||
});
|
||||
|
||||
parent.querySelectorAll<HTMLElement>('[data-action="select-dataset"]').forEach((element) => {
|
||||
element.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const tableId = element.dataset.tableId;
|
||||
const option = state.options.find((item) => String(item.datasetRef.tableId) === String(tableId));
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
node.data = {
|
||||
...(node.data || {}),
|
||||
datasetRef: option.datasetRef,
|
||||
sourceName: option.sourceName,
|
||||
sourceType: option.sourceType,
|
||||
};
|
||||
updateNodeData?.(node.id, {
|
||||
datasetRef: option.datasetRef,
|
||||
sourceName: option.sourceName,
|
||||
sourceType: option.sourceType,
|
||||
});
|
||||
rerenderSaveNode(parent, node, flowInstance);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { api } from '#/api/request';
|
||||
|
||||
export interface DatasetRefPayload {
|
||||
sourceId: number | string | null;
|
||||
catalogId?: number | string | null;
|
||||
catalogName?: string;
|
||||
tableId: number | string | null;
|
||||
tableName: string;
|
||||
versionId?: number | string | null;
|
||||
}
|
||||
|
||||
export interface ManagedDatasetFieldOption {
|
||||
fieldName: string;
|
||||
fieldDesc?: string;
|
||||
fieldType?: string;
|
||||
}
|
||||
|
||||
export interface ManagedDatasetSchema {
|
||||
tableName?: string;
|
||||
tableDesc?: string;
|
||||
fields: ManagedDatasetFieldOption[];
|
||||
}
|
||||
|
||||
export interface ManagedDatasetOption {
|
||||
label: string;
|
||||
value: number | string;
|
||||
keywords: string;
|
||||
sourceName: string;
|
||||
sourceType?: string;
|
||||
catalogName: string;
|
||||
tableName: string;
|
||||
datasetRef: DatasetRefPayload;
|
||||
}
|
||||
|
||||
export interface ManagedDatasetSourceOption {
|
||||
sourceId: number | string;
|
||||
sourceName: string;
|
||||
sourceType?: string;
|
||||
label: string;
|
||||
keywords: string;
|
||||
tables: ManagedDatasetOption[];
|
||||
}
|
||||
|
||||
const SOURCE_MISSING_MESSAGE = '连接不存在';
|
||||
const SOURCE_UNAVAILABLE_MESSAGE = '当前连接不可用,请检查连接配置后重试';
|
||||
|
||||
function shouldSkipSourceError(error: any) {
|
||||
const responseData = error?.response?.data ?? {};
|
||||
const message = String(responseData?.message ?? error?.message ?? '');
|
||||
|
||||
return (
|
||||
message.includes(SOURCE_MISSING_MESSAGE) ||
|
||||
message.includes(SOURCE_UNAVAILABLE_MESSAGE)
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeManagedDatasetOptions(options: ManagedDatasetOption[]) {
|
||||
const uniqueOptions = new Map<string, ManagedDatasetOption>();
|
||||
for (const option of options || []) {
|
||||
const key = option.datasetRef?.tableId != null
|
||||
? String(option.datasetRef.tableId)
|
||||
: [
|
||||
option.datasetRef?.sourceId ?? '',
|
||||
option.datasetRef?.catalogId ?? '',
|
||||
option.tableName ?? '',
|
||||
].join(':');
|
||||
if (!uniqueOptions.has(key)) {
|
||||
uniqueOptions.set(key, option);
|
||||
}
|
||||
}
|
||||
return Array.from(uniqueOptions.values());
|
||||
}
|
||||
|
||||
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 options: ManagedDatasetOption[] = [];
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const catalogRes = await api.get('/api/v1/datacenterSource/catalogs', {
|
||||
params: {
|
||||
sourceId: source.id,
|
||||
},
|
||||
});
|
||||
const catalogs = catalogRes.data || [];
|
||||
for (const catalog of catalogs) {
|
||||
const tableRes = await api.get('/api/v1/datacenterDataset/managedTables', {
|
||||
params: {
|
||||
sourceId: source.id,
|
||||
catalogId: catalog.id,
|
||||
},
|
||||
});
|
||||
const tables = tableRes.data || [];
|
||||
for (const table of tables) {
|
||||
const label = `${source.sourceName} / ${catalog.catalogName} / ${table.tableName}`;
|
||||
options.push({
|
||||
label,
|
||||
value: table.id,
|
||||
keywords: `${source.sourceName} ${catalog.catalogName} ${table.tableName}`.toLowerCase(),
|
||||
sourceName: source.sourceName,
|
||||
sourceType: source.sourceType,
|
||||
catalogName: catalog.catalogName,
|
||||
tableName: table.tableName,
|
||||
datasetRef: {
|
||||
sourceId: source.id,
|
||||
catalogId: catalog.id,
|
||||
catalogName: catalog.catalogName,
|
||||
tableId: table.id,
|
||||
tableName: table.tableName,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (shouldSkipSourceError(error)) {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return dedupeManagedDatasetOptions(options);
|
||||
}
|
||||
|
||||
export function groupManagedDatasetOptions(
|
||||
options: ManagedDatasetOption[],
|
||||
): ManagedDatasetSourceOption[] {
|
||||
const grouped = new Map<number | string, ManagedDatasetSourceOption>();
|
||||
for (const option of dedupeManagedDatasetOptions(options || [])) {
|
||||
const sourceId = option.datasetRef?.sourceId;
|
||||
if (sourceId == null) {
|
||||
continue;
|
||||
}
|
||||
if (!grouped.has(sourceId)) {
|
||||
grouped.set(sourceId, {
|
||||
sourceId,
|
||||
sourceName: option.sourceName,
|
||||
sourceType: option.sourceType,
|
||||
label: option.sourceName,
|
||||
keywords: option.sourceName.toLowerCase(),
|
||||
tables: [],
|
||||
});
|
||||
}
|
||||
grouped.get(sourceId)!.tables.push(option);
|
||||
}
|
||||
return Array.from(grouped.values()).map((item) => ({
|
||||
...item,
|
||||
keywords: `${item.sourceName} ${item.tables
|
||||
.map((table) => `${table.catalogName} ${table.tableName}`)
|
||||
.join(' ')}`.toLowerCase(),
|
||||
tables: item.tables.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function loadManagedDatasetSchema(
|
||||
datasetRef?: DatasetRefPayload | null,
|
||||
): Promise<ManagedDatasetSchema> {
|
||||
if (!datasetRef?.tableId) {
|
||||
return {
|
||||
tableName: datasetRef?.tableName,
|
||||
fields: [],
|
||||
};
|
||||
}
|
||||
const res = await api.get('/api/v1/datacenterDataset/schema', {
|
||||
params: datasetRef,
|
||||
});
|
||||
const data = res.data || {};
|
||||
const fields = Array.isArray(data.fields)
|
||||
? data.fields.map((field: any) => ({
|
||||
fieldName: field.fieldName,
|
||||
fieldDesc: field.fieldDesc,
|
||||
fieldType: field.jdbcType || field.fieldType,
|
||||
}))
|
||||
: [];
|
||||
return {
|
||||
tableName: data.table?.tableName || datasetRef.tableName,
|
||||
tableDesc: data.table?.tableDesc,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import downloadNode from './downloadNode';
|
||||
import makeFileNode from './makeFileNode';
|
||||
import nodeNames from './nodeNames';
|
||||
import { PluginNode } from './pluginNode';
|
||||
import { SaveToDatacenterNode } from './saveToDatacenter';
|
||||
import { SearchDatacenterNode } from './searchDatacenter';
|
||||
import sqlNode from './sqlNode';
|
||||
import { SaveDatasetNode } from './saveDataset';
|
||||
import { SearchDatasetNode } from './searchDataset';
|
||||
import { WorkflowNode } from './workflowNode';
|
||||
|
||||
export interface CustomNodeOptions {
|
||||
@@ -14,16 +13,15 @@ export interface CustomNodeOptions {
|
||||
export const getCustomNode = async (options: CustomNodeOptions) => {
|
||||
const pluginNode = PluginNode({ onChosen: options.handleChosen });
|
||||
const workflowNode = WorkflowNode({ onChosen: options.handleChosen });
|
||||
const searchDatacenterNode = await SearchDatacenterNode();
|
||||
const saveToDatacenterNode = await SaveToDatacenterNode();
|
||||
const searchDatasetNode = await SearchDatasetNode();
|
||||
const saveDatasetNode = await SaveDatasetNode();
|
||||
return {
|
||||
...docNode,
|
||||
...makeFileNode,
|
||||
...downloadNode,
|
||||
...sqlNode,
|
||||
[nodeNames.pluginNode]: pluginNode,
|
||||
[nodeNames.workflowNode]: workflowNode,
|
||||
[nodeNames.searchDatacenterNode]: searchDatacenterNode,
|
||||
[nodeNames.saveToDatacenterNode]: saveToDatacenterNode,
|
||||
[nodeNames.searchDatasetNode]: searchDatasetNode,
|
||||
[nodeNames.saveDatasetNode]: saveDatasetNode,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,9 +2,8 @@ export default {
|
||||
documentNode: 'document-node',
|
||||
makeFileNode: 'make-file',
|
||||
downloadNode: 'download-node',
|
||||
sqlNode: 'sql-node',
|
||||
pluginNode: 'plugin-node',
|
||||
workflowNode: 'workflow-node',
|
||||
searchDatacenterNode: 'search-datacenter-node',
|
||||
saveToDatacenterNode: 'save-to-datacenter-node',
|
||||
searchDatasetNode: 'search-dataset-node',
|
||||
saveDatasetNode: 'save-dataset-node',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { rerenderSaveNode } from './datasetNodeRenderer';
|
||||
|
||||
export const SaveDatasetNode = async () => {
|
||||
return {
|
||||
title: $t('aiWorkflow.saveDataset'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.saveDataset'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 3H19C20.1046 3 21 3.89543 21 5V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V5C3 3.89543 3.89543 3 5 3ZM7 5V9H17V5H7ZM7 13V19H17V13H7Z"></path></svg>',
|
||||
sortNo: 812,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [
|
||||
{
|
||||
name: 'saveList',
|
||||
title: $t('aiWorkflow.dataToBeSaved'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.dataToBeSaved'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'successRows',
|
||||
title: $t('aiWorkflow.successInsertedRecords'),
|
||||
dataType: 'Number',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.successInsertedRecords'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
render: rerenderSaveNode,
|
||||
onUpdate: rerenderSaveNode,
|
||||
};
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { getOptions } from '@easyflow/utils';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const SaveToDatacenterNode = async () => {
|
||||
const res = await api.get('/api/v1/datacenterTable/list');
|
||||
|
||||
return {
|
||||
title: $t('aiWorkflow.saveData'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.saveData'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 19V9H4V19H11ZM11 7V4C11 3.44772 11.4477 3 12 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V8C2 7.44772 2.44772 7 3 7H11ZM13 5V19H20V5H13ZM5 16H10V18H5V16ZM14 16H19V18H14V16ZM14 13H19V15H14V13ZM14 10H19V12H14V10ZM5 13H10V15H5V13Z"></path></svg>',
|
||||
sortNo: 812,
|
||||
parametersAddEnable: false,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [
|
||||
{
|
||||
name: 'saveList',
|
||||
title: $t('aiWorkflow.dataToBeSaved'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.dataToBeSaved'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'successRows',
|
||||
title: $t('aiWorkflow.successInsertedRecords'),
|
||||
dataType: 'Number',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.successInsertedRecords'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
forms: [
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.dataTable'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '',
|
||||
description: $t('aiWorkflow.descriptions.dataTable'),
|
||||
name: 'tableId',
|
||||
defaultValue: '',
|
||||
options: getOptions('tableName', 'id', res.data),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { getOptions } from '@easyflow/utils';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
export const SearchDatacenterNode = async () => {
|
||||
const res = await api.get('/api/v1/datacenterTable/list');
|
||||
|
||||
return {
|
||||
title: $t('aiWorkflow.queryData'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.queryData'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 2C15.968 2 20 6.032 20 11C20 15.968 15.968 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2ZM11 18C14.8675 18 18 14.8675 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18ZM19.4853 18.0711L22.3137 20.8995L20.8995 22.3137L18.0711 19.4853L19.4853 18.0711Z"></path></svg>',
|
||||
sortNo: 813,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: false,
|
||||
parameters: [],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'rows',
|
||||
title: $t('aiWorkflow.queryResult'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.queryResult'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: false,
|
||||
},
|
||||
],
|
||||
forms: [
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.dataTable'),
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: '',
|
||||
description: $t('aiWorkflow.descriptions.dataTable'),
|
||||
name: 'tableId',
|
||||
defaultValue: '',
|
||||
options: getOptions('tableName', 'id', res.data),
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.filterConditions'),
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
templateSupport: true,
|
||||
label: "如:name='张三' and age=21 or field = {{流程变量}}",
|
||||
description: '',
|
||||
name: 'where',
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
type: 'heading',
|
||||
label: $t('aiWorkflow.limit'),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
label: '',
|
||||
description: '',
|
||||
name: 'limit',
|
||||
defaultValue: '10',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { rerenderSearchNode } from './datasetNodeRenderer';
|
||||
|
||||
export const SearchDatasetNode = async () => {
|
||||
return {
|
||||
title: $t('aiWorkflow.queryDataset'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.queryDataset'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 4H20V6H4V4ZM4 9H20V11H4V9ZM4 14H14V16H4V14ZM4 19H14V21H4V19ZM17 14H22V21H17V14Z"></path></svg>',
|
||||
sortNo: 813,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: false,
|
||||
renderFirst: true,
|
||||
parameters: [],
|
||||
forms: [
|
||||
{
|
||||
name: 'querySql',
|
||||
type: 'textarea',
|
||||
templateSupport: true,
|
||||
label: 'SQL',
|
||||
placeholder: $t('aiWorkflow.descriptions.enterSQL'),
|
||||
attrs: {
|
||||
rows: 6,
|
||||
},
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'data',
|
||||
title: 'data',
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: 'data',
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
render: rerenderSearchNode,
|
||||
onUpdate: rerenderSearchNode,
|
||||
};
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import nodeNames from './nodeNames';
|
||||
|
||||
export default {
|
||||
[nodeNames.sqlNode]: {
|
||||
title: $t('aiWorkflow.sqlQuery'),
|
||||
group: 'base',
|
||||
description: $t('aiWorkflow.descriptions.sqlQuery'),
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgba(37,99,235,1)"><path d="M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z"></path></svg>',
|
||||
sortNo: 803,
|
||||
parametersAddEnable: true,
|
||||
outputDefsAddEnable: true,
|
||||
parameters: [],
|
||||
forms: [
|
||||
{
|
||||
name: 'sql',
|
||||
type: 'textarea',
|
||||
templateSupport: true,
|
||||
label: 'SQL',
|
||||
placeholder: $t('aiWorkflow.descriptions.enterSQL'),
|
||||
},
|
||||
],
|
||||
outputDefs: [
|
||||
{
|
||||
name: 'queryData',
|
||||
title: $t('aiWorkflow.queryResult'),
|
||||
dataType: 'Array',
|
||||
dataTypeDisabled: true,
|
||||
required: true,
|
||||
parametersAddEnable: false,
|
||||
description: $t('aiWorkflow.descriptions.queryResultJson'),
|
||||
deleteDisabled: true,
|
||||
nameDisabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -77,6 +77,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isMarkupIcon(icon?: string) {
|
||||
return typeof icon === 'string' && icon.trim().startsWith('<');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#snippet renderDefaultItems(items: SelectItem[], depth = 0)}
|
||||
@@ -97,7 +101,13 @@
|
||||
{#if group.selectable === false}
|
||||
<div class="tf-select-model-group-title">
|
||||
{#if group.icon}
|
||||
<span class="tf-select-model-group-icon">{@html group.icon}</span>
|
||||
<span class="tf-select-model-group-icon">
|
||||
{#if isMarkupIcon(group.icon)}
|
||||
{@html group.icon}
|
||||
{:else}
|
||||
<img src={group.icon} alt="" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span>{group.label}</span>
|
||||
</div>
|
||||
@@ -105,7 +115,11 @@
|
||||
<button class="tf-select-model-item {value.includes(model.value) ? 'active' : ''}" onclick={(e) => { e.stopPropagation(); handlerOnSelect(model); }}>
|
||||
<div class="tf-select-model-icon">
|
||||
{#if model.icon}
|
||||
{@html model.icon}
|
||||
{#if isMarkupIcon(model.icon)}
|
||||
{@html model.icon}
|
||||
{:else}
|
||||
<img src={model.icon} alt="" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="tf-select-model-avatar">{group.label ? group.label.toString().charAt(0) : 'M'}</div>
|
||||
{/if}
|
||||
@@ -129,7 +143,11 @@
|
||||
<button class="tf-select-model-item {value.includes(group.value) ? 'active' : ''}" onclick={(e) => { e.stopPropagation(); handlerOnSelect(group); }}>
|
||||
<div class="tf-select-model-icon">
|
||||
{#if group.icon}
|
||||
{@html group.icon}
|
||||
{#if isMarkupIcon(group.icon)}
|
||||
{@html group.icon}
|
||||
{:else}
|
||||
<img src={group.icon} alt="" />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="tf-select-model-avatar">{group.label ? group.label.toString().charAt(0) : 'M'}</div>
|
||||
{/if}
|
||||
@@ -200,7 +218,11 @@
|
||||
</span>
|
||||
{:else if variant === 'model' && item.icon}
|
||||
<span class="tf-select-item-icon-input-model">
|
||||
{@html item.icon}
|
||||
{#if isMarkupIcon(item.icon)}
|
||||
{@html item.icon}
|
||||
{:else}
|
||||
<img src={item.icon} alt="" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
||||
@@ -217,7 +239,11 @@
|
||||
</span>
|
||||
{:else if variant === 'model' && item.icon}
|
||||
<span class="tf-select-item-icon-input-model">
|
||||
{@html item.icon}
|
||||
{#if isMarkupIcon(item.icon)}
|
||||
{@html item.icon}
|
||||
{:else}
|
||||
<img src={item.icon} alt="" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
|
||||
@@ -305,6 +331,12 @@
|
||||
</div>
|
||||
|
||||
<style lang="less">
|
||||
:global(.tf-select > div:first-child) {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tf-select-default-wrapper {
|
||||
display: flex;
|
||||
@@ -525,13 +557,15 @@
|
||||
box-sizing: border-box;
|
||||
max-height: 480px;
|
||||
z-index: 99999;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.tf-select-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
|
||||
&.tf-select-primary-list {
|
||||
width: 100%; /* Default fills the wrapper, which is minWidth-constrained by the input */
|
||||
@@ -543,13 +577,13 @@
|
||||
.tf-select-wrapper:has(.tf-select-secondary-list) &.tf-select-primary-list {
|
||||
/* Let it take the width of the input minus borders/paddings if needed, but minWidth handles it mostly */
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
&.tf-select-secondary-list {
|
||||
min-width: 220px;
|
||||
min-width: 188px;
|
||||
background: var(--tf-bg-surface);
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
border-left: 1px solid var(--tf-bg-muted);
|
||||
animation: slideIn 0.2s ease-out;
|
||||
box-sizing: border-box;
|
||||
@@ -561,6 +595,8 @@
|
||||
|
||||
.tf-select-item-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tf-select-item-children {
|
||||
@@ -646,8 +682,10 @@
|
||||
.tf-parameter-label-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-right: 4px;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.tf-select-item-icon-input {
|
||||
width: 18px;
|
||||
@@ -692,12 +730,16 @@
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tf-parameter-name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tf-parameter-expand-icon {
|
||||
@@ -709,16 +751,21 @@
|
||||
|
||||
.tf-parameter-name {
|
||||
color: inherit;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tf-parameter-type {
|
||||
background: var(--tf-bg-tag);
|
||||
color: var(--tf-text-secondary);
|
||||
padding: 2px 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -49,9 +49,13 @@
|
||||
<style lang="less">
|
||||
.input-container {
|
||||
display: grid;
|
||||
grid-template-columns: 40% 50% 10%;
|
||||
row-gap: 5px;
|
||||
column-gap: 3px;
|
||||
grid-template-columns: 124px minmax(0, 1fr) 22px;
|
||||
row-gap: 6px;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 318px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.none-params {
|
||||
font-size: 12px;
|
||||
@@ -61,16 +65,17 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 5px);
|
||||
width: 100%;
|
||||
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-header {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -148,6 +148,8 @@
|
||||
.input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.input-more-setting {
|
||||
@@ -171,4 +173,3 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -41,9 +41,13 @@
|
||||
<style lang="less">
|
||||
.input-container {
|
||||
display: grid;
|
||||
grid-template-columns: 40% 50% 10%;
|
||||
row-gap: 5px;
|
||||
column-gap: 3px;
|
||||
grid-template-columns: 124px minmax(0, 1fr) 22px;
|
||||
row-gap: 6px;
|
||||
column-gap: 4px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 318px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.none-params {
|
||||
font-size: 12px;
|
||||
@@ -53,16 +57,16 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 5px);
|
||||
width: 100%;
|
||||
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-header {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
{/if}
|
||||
|
||||
|
||||
{#if customNode.renderFirst}
|
||||
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
|
||||
{/if}
|
||||
|
||||
{#if forms}
|
||||
{#each forms as form}
|
||||
{#if form.type === 'input'}
|
||||
@@ -201,7 +205,9 @@
|
||||
{/if}
|
||||
|
||||
|
||||
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
|
||||
{#if !customNode.renderFirst}
|
||||
<div bind:this={container} style={customNode.rootStyle||""} class={customNode.rootClass}></div>
|
||||
{/if}
|
||||
|
||||
|
||||
{#if customNode.outputDefsEnable !== false}
|
||||
@@ -279,4 +285,3 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {type NodeProps, useNodesData, useStore, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {Button, FloatingTrigger, Heading, Select} from '../base';
|
||||
import {MenuButton} from '../base/index.js';
|
||||
import RefParameterList from '../core/RefParameterList.svelte';
|
||||
@@ -20,8 +20,42 @@
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
let currentNode = useNodesData(currentNodeId);
|
||||
const { addParameter } = useAddParameter();
|
||||
const { nodes } = $derived(useStore());
|
||||
const editorParameters = $derived.by(() => {
|
||||
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||
const parameters = [
|
||||
...(((currentNode?.current?.data?.parameters as Array<any>) || data.parameters || []) as Array<any>)
|
||||
];
|
||||
if (queryContextNodeIds.length > 0) {
|
||||
parameters.push({
|
||||
id: 'queryDataContext',
|
||||
name: 'queryDataContext',
|
||||
dataType: 'String',
|
||||
description: '数据查询规则与连接表摘要',
|
||||
required: false,
|
||||
nameDisabled: true,
|
||||
dataTypeDisabled: true,
|
||||
deleteDisabled: true
|
||||
});
|
||||
}
|
||||
return parameters;
|
||||
});
|
||||
const queryContextOptions = $derived.by(() => {
|
||||
return (nodes || [])
|
||||
.filter((node: any) => node?.id !== currentNodeId && node?.type === 'search-dataset-node')
|
||||
.map((node: any) => ({
|
||||
label: node?.data?.title || '查询数据',
|
||||
value: node.id,
|
||||
displayLabel: node?.data?.title || '查询数据',
|
||||
description: node?.data?.sourceName ? '连接服务:' + node.data.sourceName : '未选择连接服务',
|
||||
}));
|
||||
});
|
||||
const queryContextNodeIds = $derived.by(() => {
|
||||
const ids = (currentNode?.current?.data?.queryContextNodeIds as Array<any>) || data.queryContextNodeIds || [];
|
||||
return Array.isArray(ids)
|
||||
? ids
|
||||
.map((item: any) => (item == null ? '' : String(item)))
|
||||
.filter((item: string) => item.trim().length > 0)
|
||||
: [];
|
||||
});
|
||||
|
||||
const options = getOptions();
|
||||
@@ -29,33 +63,29 @@
|
||||
let llmArray = $state<SelectItem[]>([]);
|
||||
onMount(async () => {
|
||||
const newLLMs = await options.provider?.llm?.();
|
||||
|
||||
|
||||
const isFlat = newLLMs?.every(item => !item.children);
|
||||
|
||||
|
||||
if (isFlat && newLLMs && newLLMs.length > 0) {
|
||||
const grouped = new Map<string, SelectItem[]>();
|
||||
for (const llm of newLLMs) {
|
||||
// If it still has a slash, parse it; otherwise, check if there's a custom logic we can infer brand.
|
||||
// In WorkflowDesign we pass `item.modelProvider?.providerName` via some other way, but here it's flat.
|
||||
// Actually, the label is just the title now (e.g. 'deepseek-chat').
|
||||
// Wait, LLMNode doesn't know the brand unless it's in the string or we modify WorkflowDesign to pass `brand`.
|
||||
// Let's modify WorkflowDesign to pass `brand` instead.
|
||||
let brand = (llm as any).brand || '其他';
|
||||
let modelName = llm.label;
|
||||
|
||||
const modelName = typeof llm.label === 'string'
|
||||
? llm.label
|
||||
: ((llm.displayLabel as string | undefined) || '模型');
|
||||
|
||||
if (!grouped.has(brand)) {
|
||||
grouped.set(brand, []);
|
||||
}
|
||||
grouped.get(brand)!.push({
|
||||
...llm,
|
||||
label: modelName,
|
||||
displayLabel: modelName // 外部选中时也只显示模型名称
|
||||
displayLabel: modelName
|
||||
});
|
||||
}
|
||||
|
||||
const treeArray: SelectItem[] = [];
|
||||
for (const [brand, models] of grouped) {
|
||||
// Try to get a representative icon for the brand from its children
|
||||
let groupIcon = undefined;
|
||||
if (models.length > 0) {
|
||||
const modelWithIcon = models.find(m => m.icon);
|
||||
@@ -72,9 +102,9 @@
|
||||
children: models
|
||||
});
|
||||
}
|
||||
llmArray.push(...treeArray);
|
||||
llmArray = treeArray;
|
||||
} else {
|
||||
llmArray.push(...(newLLMs || []));
|
||||
llmArray = [...(newLLMs || [])];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -121,6 +151,26 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const validIds = new Set(queryContextOptions.map((item) => String(item.value)));
|
||||
const normalized = queryContextNodeIds.filter((item) => validIds.has(item));
|
||||
if (normalized.length !== queryContextNodeIds.length) {
|
||||
updateNodeData(currentNodeId, {
|
||||
queryContextNodeIds: normalized
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const toggleQueryContextNode = (nodeId: string) => {
|
||||
const currentIds = [...queryContextNodeIds];
|
||||
const exists = currentIds.includes(nodeId);
|
||||
updateNodeData(currentNodeId, {
|
||||
queryContextNodeIds: exists
|
||||
? currentIds.filter((item) => item !== nodeId)
|
||||
: [...currentIds, nodeId]
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -159,6 +209,20 @@
|
||||
|
||||
<RefParameterList dataKeyName="images" noneParameterText="无图片参数" />
|
||||
|
||||
<Heading level={3} mt="10px">查询数据信息</Heading>
|
||||
<div class="setting-item">
|
||||
<Select
|
||||
items={queryContextOptions}
|
||||
multiple={true}
|
||||
style="width: 100%"
|
||||
placeholder="请选择查询数据节点"
|
||||
onSelect={(item)=>{
|
||||
toggleQueryContextNode(String(item.value));
|
||||
}}
|
||||
value={queryContextNodeIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Heading level={3} mt="10px">模型设置</Heading>
|
||||
<div class="setting-title">模型</div>
|
||||
<div class="setting-item">
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
|
||||
|
||||
.tf-select {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
&-input {
|
||||
display: flex;
|
||||
border: 1px solid var(--tf-border-color);
|
||||
@@ -95,7 +99,8 @@
|
||||
|
||||
&-value {
|
||||
height: 100%;
|
||||
min-width: 10px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user