|
|
@@ -4,9 +4,14 @@ import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.fs.company.service.llm.MultiModelRouter;
|
|
|
+import com.fs.common.core.domain.AjaxResult;
|
|
|
+import com.fs.company.service.workflow.ConditionEvaluator;
|
|
|
import com.fs.company.service.workflow.DynamicNodeExecutor;
|
|
|
import com.fs.company.service.workflow.LobsterNodeTypeService;
|
|
|
+import com.fs.company.service.workflow.LobsterWorkflowExecutor;
|
|
|
import com.fs.company.service.workflow.QualityScoringService;
|
|
|
+import com.fs.company.service.workflow.ToolCallFramework;
|
|
|
+import com.fs.company.service.workflow.vector.VectorPatternMatcher;
|
|
|
import com.fs.company.service.workflow.channel.MessageChannelRequest;
|
|
|
import com.fs.company.service.workflow.channel.MessageChannelResult;
|
|
|
import com.fs.company.service.workflow.channel.MessageChannelRouter;
|
|
|
@@ -15,6 +20,7 @@ import com.fs.company.domain.LobsterWorkflowNodeType;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.context.annotation.Lazy;
|
|
|
import org.springframework.http.HttpEntity;
|
|
|
import org.springframework.http.HttpHeaders;
|
|
|
import org.springframework.http.HttpMethod;
|
|
|
@@ -30,12 +36,6 @@ import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
|
-import javax.annotation.PostConstruct;
|
|
|
-import java.util.HashMap;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
-import java.util.concurrent.ConcurrentHashMap;
|
|
|
-
|
|
|
@Service
|
|
|
public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
|
|
|
@@ -65,6 +65,19 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
@Autowired(required = false)
|
|
|
private com.fs.company.service.workflow.pay.PayService payService;
|
|
|
|
|
|
+ @Autowired(required = false)
|
|
|
+ private ConditionEvaluator conditionEvaluator;
|
|
|
+
|
|
|
+ @Autowired(required = false)
|
|
|
+ private VectorPatternMatcher vectorPatternMatcher;
|
|
|
+
|
|
|
+ @Autowired(required = false)
|
|
|
+ @Lazy
|
|
|
+ private LobsterWorkflowExecutor workflowExecutor;
|
|
|
+
|
|
|
+ @Autowired(required = false)
|
|
|
+ private ToolCallFramework toolCallFramework;
|
|
|
+
|
|
|
private final RestTemplate restTemplate = new RestTemplate();
|
|
|
|
|
|
private final ConcurrentHashMap<Integer, NodeHandler> handlers = new ConcurrentHashMap<>();
|
|
|
@@ -80,8 +93,9 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
handlers.put(3, this::handleJudgmentNode);
|
|
|
handlers.put(4, this::handleWaitNode);
|
|
|
handlers.put(5, this::handleEndNode);
|
|
|
+ handlers.put(6, this::handlePromotionEndNode);
|
|
|
handlers.put(7, this::handleOrderSuccessNode);
|
|
|
- handlers.put(8, this::handleCouponNode);
|
|
|
+ handlers.put(8, this::handleOrderConfirmNode);
|
|
|
handlers.put(9, this::handleTagOperationNode);
|
|
|
handlers.put(10, this::handleCareNode);
|
|
|
handlers.put(11, this::handleSurveyNode);
|
|
|
@@ -91,15 +105,27 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
handlers.put(20, this::handleIntentRecognitionNode);
|
|
|
handlers.put(21, this::handleTakeoverDetectNode);
|
|
|
handlers.put(22, this::handleQualityCheckNode);
|
|
|
+ handlers.put(23, this::handleKnowledgeRetrievalNode);
|
|
|
+ handlers.put(24, this::handleProductRecommendNode);
|
|
|
+ handlers.put(25, this::handleTagMatchNode);
|
|
|
handlers.put(30, this::handleQwMessageNode);
|
|
|
handlers.put(31, this::handleImMessageNode);
|
|
|
+ handlers.put(32, this::handleTimedDelayNode);
|
|
|
+ handlers.put(33, this::handleAiChatNode);
|
|
|
+ handlers.put(34, this::handleSmsMessageNode);
|
|
|
+ handlers.put(35, this::handleEmailMessageNode);
|
|
|
handlers.put(40, this::handleVariableAssignNode);
|
|
|
+ handlers.put(41, this::handleAddTagNode);
|
|
|
handlers.put(42, this::handleWebhookNode);
|
|
|
+ handlers.put(43, this::handleSubWorkflowNode);
|
|
|
+ handlers.put(44, this::handleCreateTaskNode);
|
|
|
handlers.put(50, this::handleSopExecuteNode);
|
|
|
handlers.put(51, this::handleCidTaskNode);
|
|
|
handlers.put(52, this::handleProductPushNode);
|
|
|
handlers.put(53, this::handleLogisticsNotifyNode);
|
|
|
handlers.put(100, this::handleExternalApiNode);
|
|
|
+ handlers.put(200, this::handleCustomNode);
|
|
|
+ handlers.put(201, this::handleCustomNode);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -221,6 +247,18 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
* 简化评分:success=基础 60,有 outputVariables +20,有 messageToSend +15,errorMessage 为空 +5
|
|
|
*/
|
|
|
private double scoreResult(NodeExecutionResult r, ExecutionContext ctx) {
|
|
|
+ if (qualityScoringService != null && r.getMessageToSend() != null && ctx.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ String userQ = ctx.getLastMessage() != null ? ctx.getLastMessage() : "";
|
|
|
+ QualityScoringService.DetailedScore ds = qualityScoringService.score(
|
|
|
+ ctx.getCompanyId(), r.getMessageToSend(), userQ, null, null, null);
|
|
|
+ if (ds != null && ds.getTotalScore() > 0) {
|
|
|
+ return Math.min(100, ds.getTotalScore() * 100.0 / QualityScoringService.Threshold.FULL_SCORE);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("qualityScoringService fallback: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
double s = r.isSuccess() ? 60 : 0;
|
|
|
if (r.getOutputVariables() != null) s += 20;
|
|
|
if (r.getMessageToSend() != null && !r.getMessageToSend().isEmpty()) s += 15;
|
|
|
@@ -384,13 +422,25 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
try {
|
|
|
JSONObject config = JSON.parseObject(nodeConfig);
|
|
|
String condition = config.getString("conditionExpr");
|
|
|
+ if (condition == null) condition = config.getString("condition");
|
|
|
String trueNext = config.getString("trueNextNode");
|
|
|
String falseNext = config.getString("falseNextNode");
|
|
|
-
|
|
|
- boolean conditionResult = evaluateCondition(condition, context.getVariables());
|
|
|
-
|
|
|
+ String defaultNext = config.getString("defaultNextNode");
|
|
|
+
|
|
|
NodeExecutionResult result = NodeExecutionResult.success();
|
|
|
- result.setNextNodeCode(conditionResult ? trueNext : falseNext);
|
|
|
+ if (conditionEvaluator != null && condition != null && !condition.isEmpty()) {
|
|
|
+ String nextCode = conditionEvaluator.evaluate(condition, context.getVariables(), defaultNext);
|
|
|
+ result.setNextNodeCode(nextCode != null ? nextCode : defaultNext);
|
|
|
+ } else if (conditionEvaluator != null && config.containsKey("branches")) {
|
|
|
+ String nextCode = conditionEvaluator.evaluateNextNode(context.getVariables(), config.toJSONString());
|
|
|
+ result.setNextNodeCode(nextCode);
|
|
|
+ } else {
|
|
|
+ boolean conditionResult = evaluateCondition(condition, context.getVariables());
|
|
|
+ result.setNextNodeCode(conditionResult ? trueNext : falseNext);
|
|
|
+ }
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("judgmentResult", result.getNextNodeCode());
|
|
|
+ result.setOutputVariables(outputs);
|
|
|
return result;
|
|
|
} catch (Exception e) {
|
|
|
return NodeExecutionResult.fail("判断节点处理失败: " + e.getMessage());
|
|
|
@@ -490,6 +540,22 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
return NodeExecutionResult.success();
|
|
|
}
|
|
|
|
|
|
+ /** 节点 6:促单结束 */
|
|
|
+ private NodeExecutionResult handlePromotionEndNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String reason = config != null ? config.getString("promotionReason") : null;
|
|
|
+ if (reason == null) reason = config != null ? config.getString("reason") : "促单阶段结束";
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("promotionEndReason", reason);
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(reason);
|
|
|
+ return r;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("促单结束节点处理失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private NodeExecutionResult handleOrderSuccessNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
try {
|
|
|
JSONObject config = parseConfig(nodeConfig);
|
|
|
@@ -523,7 +589,69 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // ── 节点 8:优惠券发放 ──
|
|
|
+ /** 节点 8:订单确认(本地状态,不含真实支付) */
|
|
|
+ private NodeExecutionResult handleOrderConfirmNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String orderField = config != null ? config.getString("orderField") : "orderId";
|
|
|
+ String confirmMessage = config != null ? config.getString("confirmMessage") : null;
|
|
|
+ boolean applyCoupon = config != null && config.getBooleanValue("applyCoupon");
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+
|
|
|
+ Object orderId = context.getVariables() != null ? context.getVariables().get(orderField) : null;
|
|
|
+ if (orderId == null && auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> rows = auxMapper.queryForList(
|
|
|
+ "SELECT order_no, product_name, amount, status FROM lobster_order WHERE company_id="
|
|
|
+ + context.getCompanyId() + " AND customer_id='" + sqlEscape(context.getCustomerId())
|
|
|
+ + "' ORDER BY create_time DESC LIMIT 1",
|
|
|
+ context.getCompanyId());
|
|
|
+ if (!rows.isEmpty()) {
|
|
|
+ Map<String, Object> row = rows.get(0);
|
|
|
+ orderId = row.get("order_no");
|
|
|
+ outputs.put("orderProduct", row.get("product_name"));
|
|
|
+ outputs.put("orderAmount", row.get("amount"));
|
|
|
+ outputs.put("orderStatus", row.get("status"));
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("order lookup: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ outputs.put("orderId", orderId);
|
|
|
+ outputs.put("orderConfirmed", true);
|
|
|
+ outputs.put("confirmTime", System.currentTimeMillis());
|
|
|
+
|
|
|
+ if (auxMapper != null && orderId != null && context.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ auxMapper.update(String.format(
|
|
|
+ "UPDATE lobster_order SET status='confirmed', update_time=NOW() WHERE company_id=%d AND order_no='%s'",
|
|
|
+ context.getCompanyId(), sqlEscape(orderId)));
|
|
|
+ } catch (Exception e) { logger.debug("order confirm update: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (applyCoupon && toolCallFramework != null) {
|
|
|
+ Map<String, Object> couponParams = new HashMap<>();
|
|
|
+ couponParams.put("customerId", context.getCustomerId());
|
|
|
+ if (config != null) {
|
|
|
+ if (config.getString("couponType") != null) couponParams.put("couponType", config.getString("couponType"));
|
|
|
+ if (config.getString("amount") != null) couponParams.put("amount", config.getString("amount"));
|
|
|
+ }
|
|
|
+ ToolCallFramework.ToolCallResult couponResult = toolCallFramework.executeTool(
|
|
|
+ "applyCoupon", couponParams, context.getCompanyId());
|
|
|
+ if (couponResult != null && couponResult.isSuccess() && couponResult.getData() != null) {
|
|
|
+ outputs.put("couponApplied", couponResult.getData());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String msg = confirmMessage != null ? substituteVariables(confirmMessage, context)
|
|
|
+ : (orderId != null ? "您的订单 " + orderId + " 已确认,我们会尽快为您安排。" : "订单已确认,感谢您的信任!");
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(msg);
|
|
|
+ return r;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("订单确认节点处理失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 优惠券发放(可通过订单确认节点 applyCoupon 或工具调用触发) */
|
|
|
private NodeExecutionResult handleCouponNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
try {
|
|
|
JSONObject config = parseConfig(nodeConfig);
|
|
|
@@ -872,6 +1000,429 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 节点 23:知识库检索(向量 + SQL 双通道) */
|
|
|
+ private NodeExecutionResult handleKnowledgeRetrievalNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String query = context.getLastMessage();
|
|
|
+ if (query == null || query.isEmpty()) {
|
|
|
+ query = config != null ? config.getString("defaultQuery") : "";
|
|
|
+ }
|
|
|
+ String kbCode = config != null ? config.getString("knowledgeBaseCode") : "default";
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("query", query);
|
|
|
+ List<String> snippets = new java.util.ArrayList<>();
|
|
|
+ List<Map<String, Object>> vectorHits = new java.util.ArrayList<>();
|
|
|
+
|
|
|
+ if (vectorPatternMatcher != null && query != null && !query.isEmpty() && context.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ List<VectorPatternMatcher.VectorMatchResult> matches = vectorPatternMatcher.searchSimilar(
|
|
|
+ context.getCompanyId(), "knowledge", query, 5, 0.55);
|
|
|
+ for (VectorPatternMatcher.VectorMatchResult m : matches) {
|
|
|
+ if (m.getText() != null) snippets.add(m.getText());
|
|
|
+ Map<String, Object> hit = new LinkedHashMap<>();
|
|
|
+ hit.put("key", m.getKey());
|
|
|
+ hit.put("text", m.getText());
|
|
|
+ hit.put("score", m.getScore());
|
|
|
+ if (m.getMetadata() != null) hit.put("metadata", m.getMetadata());
|
|
|
+ vectorHits.add(hit);
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("vector knowledge search: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (snippets.isEmpty() && auxMapper != null && kbCode != null && context.getCompanyId() != null && query != null && !query.isEmpty()) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> rows = auxMapper.queryForList(
|
|
|
+ "SELECT title, content FROM lobster_knowledge_chunk WHERE company_id="
|
|
|
+ + context.getCompanyId() + " AND kb_code='" + sqlEscape(kbCode)
|
|
|
+ + "' AND content LIKE '%" + sqlEscape(query) + "%' LIMIT 5",
|
|
|
+ context.getCompanyId());
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
+ Object c = row.get("content");
|
|
|
+ if (c != null) snippets.add(c.toString());
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("knowledge db lookup: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+
|
|
|
+ outputs.put("snippets", snippets);
|
|
|
+ outputs.put("vectorHits", vectorHits);
|
|
|
+ outputs.put("retrievedCount", snippets.size());
|
|
|
+
|
|
|
+ if (!snippets.isEmpty() && multiModelRouter != null) {
|
|
|
+ String contextText = String.join("\n---\n", snippets.subList(0, Math.min(3, snippets.size())));
|
|
|
+ String prompt = "根据以下知识片段回答客户问题,简洁准确。\n知识:\n" + contextText + "\n\n问题: " + query;
|
|
|
+ String answer = multiModelRouter.generateResponse(prompt, null, "knowledge_retrieval");
|
|
|
+ outputs.put("ragAnswer", answer);
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(answer != null ? answer.trim() : contextText);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+ if (snippets.isEmpty() && multiModelRouter != null && query != null && !query.isEmpty()) {
|
|
|
+ String prompt = "根据以下客户问题检索知识并回答,输出JSON: {\"answer\":\"...\"}\n问题: " + query;
|
|
|
+ String aiResp = multiModelRouter.generateResponse(prompt, null, "knowledge_retrieval");
|
|
|
+ outputs.put("ragAnswer", aiResp);
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(aiResp);
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("知识库检索失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 24:商品推荐(标签 + 品类) */
|
|
|
+ private NodeExecutionResult handleProductRecommendNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String category = config != null ? config.getString("productCategory") : null;
|
|
|
+ String tagField = config != null ? config.getString("tagField") : null;
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ List<Map<String, Object>> products = new java.util.ArrayList<>();
|
|
|
+ List<String> matchTags = new java.util.ArrayList<>();
|
|
|
+
|
|
|
+ if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ if (tagField != null && context.getVariables() != null && context.getVariables().get(tagField) != null) {
|
|
|
+ Object tv = context.getVariables().get(tagField);
|
|
|
+ matchTags.add(tv.toString());
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> tagRows = auxMapper.queryForList(
|
|
|
+ "SELECT tag_key, tag_value FROM customer_tag WHERE company_id="
|
|
|
+ + context.getCompanyId() + " AND external_user_id='" + sqlEscape(context.getCustomerId()) + "' LIMIT 10",
|
|
|
+ context.getCompanyId());
|
|
|
+ for (Map<String, Object> tr : tagRows) {
|
|
|
+ Object v = tr.get("tag_value");
|
|
|
+ if (v != null) matchTags.add(v.toString());
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("tag load for recommend: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String sql = "SELECT id, product_name, product_url, price, tags FROM lobster_product WHERE company_id="
|
|
|
+ + context.getCompanyId();
|
|
|
+ if (category != null && !category.isEmpty()) {
|
|
|
+ sql += " AND (product_name LIKE '%" + sqlEscape(category) + "%' OR tags LIKE '%" + sqlEscape(category) + "%')";
|
|
|
+ }
|
|
|
+ sql += " ORDER BY update_time DESC LIMIT 20";
|
|
|
+ List<Map<String, Object>> candidates = auxMapper.queryForList(sql, context.getCompanyId());
|
|
|
+ if (!matchTags.isEmpty()) {
|
|
|
+ for (Map<String, Object> p : candidates) {
|
|
|
+ String tags = p.get("tags") != null ? p.get("tags").toString() : "";
|
|
|
+ String name = p.get("product_name") != null ? p.get("product_name").toString() : "";
|
|
|
+ for (String mt : matchTags) {
|
|
|
+ if ((!tags.isEmpty() && tags.contains(mt)) || name.contains(mt)) {
|
|
|
+ products.add(p);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (products.size() >= 3) break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (products.isEmpty()) {
|
|
|
+ products = candidates.size() > 3 ? candidates.subList(0, 3) : candidates;
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("product recommend: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ outputs.put("products", products);
|
|
|
+ outputs.put("matchTags", matchTags);
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ if (!products.isEmpty()) {
|
|
|
+ Map<String, Object> p = products.get(0);
|
|
|
+ String name = p.get("product_name") != null ? p.get("product_name").toString() : "精选商品";
|
|
|
+ String url = p.get("product_url") != null ? p.get("product_url").toString() : "";
|
|
|
+ r.setMessageToSend("为您推荐:" + name + (url.isEmpty() ? "" : "\n" + url));
|
|
|
+ }
|
|
|
+ return r;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("商品推荐失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 25:标签匹配分支 */
|
|
|
+ private NodeExecutionResult handleTagMatchNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String requiredTags = config != null ? config.getString("requiredTags") : null;
|
|
|
+ String matchMode = config != null ? config.getString("matchMode") : "any";
|
|
|
+ String trueNext = config != null ? config.getString("trueNextNode") : null;
|
|
|
+ String falseNext = config != null ? config.getString("falseNextNode") : null;
|
|
|
+ java.util.Set<String> required = new java.util.LinkedHashSet<>();
|
|
|
+ if (requiredTags != null && !requiredTags.isEmpty()) {
|
|
|
+ for (String t : requiredTags.split("[,;|]")) {
|
|
|
+ if (!t.trim().isEmpty()) required.add(t.trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ java.util.Set<String> owned = new java.util.LinkedHashSet<>();
|
|
|
+ if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> tagRows = auxMapper.queryForList(
|
|
|
+ "SELECT tag_key, tag_value FROM customer_tag WHERE company_id="
|
|
|
+ + context.getCompanyId() + " AND external_user_id='" + sqlEscape(context.getCustomerId()) + "'",
|
|
|
+ context.getCompanyId());
|
|
|
+ for (Map<String, Object> tr : tagRows) {
|
|
|
+ if (tr.get("tag_key") != null) owned.add(tr.get("tag_key").toString());
|
|
|
+ if (tr.get("tag_value") != null) owned.add(tr.get("tag_value").toString());
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("tag match load: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ if (context.getVariables() != null) {
|
|
|
+ for (Map.Entry<String, Object> e : context.getVariables().entrySet()) {
|
|
|
+ if (e.getKey() != null && e.getKey().startsWith("tag_") && e.getValue() != null) {
|
|
|
+ owned.add(e.getValue().toString());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ boolean matched;
|
|
|
+ if (required.isEmpty()) {
|
|
|
+ matched = !owned.isEmpty();
|
|
|
+ } else if ("all".equalsIgnoreCase(matchMode)) {
|
|
|
+ matched = owned.containsAll(required);
|
|
|
+ } else {
|
|
|
+ matched = false;
|
|
|
+ for (String r : required) {
|
|
|
+ if (owned.contains(r)) { matched = true; break; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("tagMatched", matched);
|
|
|
+ outputs.put("ownedTags", new java.util.ArrayList<>(owned));
|
|
|
+ outputs.put("requiredTags", new java.util.ArrayList<>(required));
|
|
|
+ NodeExecutionResult result = NodeExecutionResult.success(outputs);
|
|
|
+ result.setNextNodeCode(matched ? trueNext : falseNext);
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("标签匹配节点失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 32:定时延迟(独立配置项) */
|
|
|
+ private NodeExecutionResult handleTimedDelayNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ long delaySeconds = config != null ? config.getLongValue("delaySeconds") : 0;
|
|
|
+ if (delaySeconds <= 0 && config != null) delaySeconds = config.getLongValue("waitSeconds");
|
|
|
+ if (delaySeconds <= 0) delaySeconds = 60;
|
|
|
+ String scheduleAt = config != null ? config.getString("scheduleAt") : null;
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ long waitUntil = scheduleAt != null ? parseScheduleAt(scheduleAt) : now + delaySeconds * 1000L;
|
|
|
+ outputs.put("waitType", "timed_delay");
|
|
|
+ outputs.put("delaySeconds", delaySeconds);
|
|
|
+ outputs.put("waitUntil", waitUntil);
|
|
|
+ outputs.put("scheduledAt", waitUntil);
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return handleWaitNode(nodeType, nodeConfig, context);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private long parseScheduleAt(String scheduleAt) {
|
|
|
+ try {
|
|
|
+ java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
|
|
+ return sdf.parse(scheduleAt).getTime();
|
|
|
+ } catch (Exception e) {
|
|
|
+ return System.currentTimeMillis() + 3600_000L;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 200/201:自定义节点(配置驱动 + AI 兜底) */
|
|
|
+ private NodeExecutionResult handleCustomNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String action = config != null ? config.getString("actionType") : "ai";
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ if ("assign".equals(action) && config.getJSONObject("assignments") != null) {
|
|
|
+ JSONObject assignments = config.getJSONObject("assignments");
|
|
|
+ for (String k : assignments.keySet()) {
|
|
|
+ outputs.put(k, substituteVariables(assignments.getString(k), context));
|
|
|
+ }
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ if (config.getString("messageTemplate") != null) {
|
|
|
+ r.setMessageToSend(substituteVariables(config.getString("messageTemplate"), context));
|
|
|
+ }
|
|
|
+ return r;
|
|
|
+ }
|
|
|
+ if ("webhook".equals(action) && config.getString("url") != null) {
|
|
|
+ return handleWebhookNode(nodeType, nodeConfig, context);
|
|
|
+ }
|
|
|
+ String prompt = config != null && config.getString("prompt") != null
|
|
|
+ ? substituteVariables(config.getString("prompt"), context)
|
|
|
+ : "执行自定义节点(type=" + nodeType + "),上下文: " + JSON.toJSONString(context.getVariables());
|
|
|
+ String reply = multiModelRouter != null
|
|
|
+ ? multiModelRouter.generateResponse(prompt, config != null ? config.getString("model") : null, "custom_node")
|
|
|
+ : "自定义节点已执行";
|
|
|
+ outputs.put("customNodeType", nodeType);
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(reply != null ? reply.trim() : "");
|
|
|
+ return r;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("自定义节点失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 33:AI 对话 */
|
|
|
+ private NodeExecutionResult handleAiChatNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String systemPrompt = config != null ? config.getString("systemPrompt") : "你是一位专业销售顾问";
|
|
|
+ String userMsg = context.getLastMessage() != null ? context.getLastMessage() : "";
|
|
|
+ String prompt = systemPrompt + "\n客户: " + userMsg + "\n请给出简洁回复(仅输出回复文本)";
|
|
|
+ String reply = multiModelRouter != null
|
|
|
+ ? multiModelRouter.generateResponse(prompt, config != null ? config.getString("model") : null, "ai_chat")
|
|
|
+ : "您好,有什么可以帮您?";
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("aiChat", true);
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(reply != null ? reply.trim() : "");
|
|
|
+ return r;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("AI对话节点失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 34:短信 */
|
|
|
+ private NodeExecutionResult handleSmsMessageNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String message = config != null ? config.getString("messageTemplate") : "";
|
|
|
+ message = substituteVariables(message, context);
|
|
|
+ String phone = config != null ? config.getString("phone") : null;
|
|
|
+ if (phone == null && context.getVariables() != null && context.getVariables().get("phone") != null) {
|
|
|
+ phone = context.getVariables().get("phone").toString();
|
|
|
+ }
|
|
|
+ MessageChannelRequest request = new MessageChannelRequest();
|
|
|
+ request.setCompanyId(context.getCompanyId());
|
|
|
+ request.setContactId(context.getCustomerId());
|
|
|
+ request.setContent(message);
|
|
|
+ request.setChannelType("sms");
|
|
|
+ request.setExtra(context.getVariables());
|
|
|
+ if (phone != null) {
|
|
|
+ Map<String, Object> extra = request.getExtra() != null ? new HashMap<>(request.getExtra()) : new HashMap<>();
|
|
|
+ extra.put("phone", phone);
|
|
|
+ request.setExtra(extra);
|
|
|
+ }
|
|
|
+ MessageChannelResult channelResult = messageChannelRouter.route(request);
|
|
|
+ NodeExecutionResult result = new NodeExecutionResult();
|
|
|
+ result.setSuccess(channelResult.isSuccess());
|
|
|
+ result.setMessageToSend(message);
|
|
|
+ if (!channelResult.isSuccess()) result.setErrorMessage(channelResult.getErrorMsg());
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("短信节点失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 35:邮件 */
|
|
|
+ private NodeExecutionResult handleEmailMessageNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String subject = config != null ? config.getString("subject") : "通知";
|
|
|
+ String body = config != null ? config.getString("bodyTemplate") : "";
|
|
|
+ body = substituteVariables(body, context);
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("emailSubject", subject);
|
|
|
+ outputs.put("emailSent", false);
|
|
|
+ if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ auxMapper.update(String.format(
|
|
|
+ "INSERT INTO lobster_email_log(company_id, customer_id, subject, body, create_time) " +
|
|
|
+ "VALUES(%d, '%s', '%s', '%s', NOW())",
|
|
|
+ context.getCompanyId(),
|
|
|
+ sqlEscape(context.getCustomerId()),
|
|
|
+ sqlEscape(subject), sqlEscape(body)));
|
|
|
+ outputs.put("emailSent", true);
|
|
|
+ } catch (Exception e) { logger.debug("email log: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ NodeExecutionResult r = NodeExecutionResult.success(outputs);
|
|
|
+ r.setMessageToSend(body);
|
|
|
+ return r;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("邮件节点失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 41:打标签(与 9 类似,独立入口) */
|
|
|
+ private NodeExecutionResult handleAddTagNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ return handleTagOperationNode(nodeType, nodeConfig, context);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 43:子流程(启动子工作流实例) */
|
|
|
+ private NodeExecutionResult handleSubWorkflowNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ Long subWorkflowId = config != null && config.get("subWorkflowId") != null
|
|
|
+ ? config.getLong("subWorkflowId") : null;
|
|
|
+ String resultVar = config != null && config.getString("resultVar") != null
|
|
|
+ ? config.getString("resultVar") : "subResult";
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("subWorkflowId", subWorkflowId);
|
|
|
+
|
|
|
+ if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ String subIdSql = subWorkflowId != null ? subWorkflowId.toString() : "NULL";
|
|
|
+ auxMapper.update(String.format(
|
|
|
+ "INSERT INTO lobster_sub_workflow_exec(company_id, parent_instance_id, sub_workflow_id, status, create_time) " +
|
|
|
+ "VALUES(%d, %d, %s, 'triggered', NOW())",
|
|
|
+ context.getCompanyId(),
|
|
|
+ context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
|
|
|
+ subIdSql));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (workflowExecutor != null && subWorkflowId != null && context.getCompanyId() != null) {
|
|
|
+ Map<String, Object> subVars = context.getVariables() != null
|
|
|
+ ? new HashMap<>(context.getVariables()) : new HashMap<>();
|
|
|
+ subVars.put("parentInstanceId", context.getWorkflowInstanceId());
|
|
|
+ subVars.put("channelType", context.getChannelType());
|
|
|
+ AjaxResult subResult = workflowExecutor.startWorkflow(
|
|
|
+ context.getCompanyId(), subWorkflowId, context.getCustomerId(), subVars);
|
|
|
+ if (subResult != null && Integer.valueOf(200).equals(subResult.get("code"))) {
|
|
|
+ Object data = subResult.get("data");
|
|
|
+ if (data instanceof com.fs.company.domain.LobsterWorkflowInstance) {
|
|
|
+ com.fs.company.domain.LobsterWorkflowInstance subInstance =
|
|
|
+ (com.fs.company.domain.LobsterWorkflowInstance) data;
|
|
|
+ outputs.put("subInstanceId", subInstance.getId());
|
|
|
+ outputs.put(resultVar, "sub_instance_" + subInstance.getId());
|
|
|
+ } else {
|
|
|
+ outputs.put(resultVar, "sub_started");
|
|
|
+ }
|
|
|
+ outputs.put("subWorkflowTriggered", true);
|
|
|
+ } else {
|
|
|
+ outputs.put("subWorkflowTriggered", false);
|
|
|
+ outputs.put(resultVar, "sub_failed");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ outputs.put("subWorkflowTriggered", subWorkflowId != null);
|
|
|
+ }
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("子流程节点失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 节点 44:创建任务 */
|
|
|
+ private NodeExecutionResult handleCreateTaskNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String taskTitle = config != null ? config.getString("taskTitle") : "跟进任务";
|
|
|
+ String taskContent = config != null ? config.getString("taskContent") : "";
|
|
|
+ taskContent = substituteVariables(taskContent, context);
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ outputs.put("taskTitle", taskTitle);
|
|
|
+ if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ auxMapper.update(String.format(
|
|
|
+ "INSERT INTO lobster_task(company_id, instance_id, customer_id, task_title, task_content, status, create_time) " +
|
|
|
+ "VALUES(%d, %d, '%s', '%s', '%s', 'pending', NOW())",
|
|
|
+ context.getCompanyId(),
|
|
|
+ context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
|
|
|
+ sqlEscape(context.getCustomerId()),
|
|
|
+ sqlEscape(taskTitle), sqlEscape(taskContent)));
|
|
|
+ outputs.put("taskCreated", true);
|
|
|
+ }
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("创建任务失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/** 安全解析 config,兼容 null 和空字符串 */
|
|
|
private JSONObject parseConfig(String cfg) {
|
|
|
if (cfg == null || cfg.isEmpty() || "{}".equals(cfg)) return new JSONObject();
|
|
|
@@ -898,39 +1449,232 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
|
|
|
}
|
|
|
|
|
|
private NodeExecutionResult handleWebhookNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
- Map<String, Object> outputs = new HashMap<>();
|
|
|
- outputs.put("webhookCalled", true);
|
|
|
- return NodeExecutionResult.success(outputs);
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ Map<String, Object> outputs = invokeHttpFromConfig(config, context, "webhookUrl", "url");
|
|
|
+ outputs.put("webhookCalled", true);
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("Webhook调用失败: " + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private NodeExecutionResult handleSopExecuteNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
- Map<String, Object> outputs = new HashMap<>();
|
|
|
- outputs.put("sopExecuted", true);
|
|
|
- return NodeExecutionResult.success(outputs);
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ String sopId = config != null ? config.getString("sopId") : null;
|
|
|
+ String sopName = config != null ? config.getString("sopName") : "default_sop";
|
|
|
+ outputs.put("sopId", sopId);
|
|
|
+ outputs.put("sopName", sopName);
|
|
|
+ if (config != null && (config.containsKey("webhookUrl") || config.containsKey("url"))) {
|
|
|
+ outputs.putAll(invokeHttpFromConfig(config, context, "webhookUrl", "url"));
|
|
|
+ } else if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ auxMapper.update(String.format(
|
|
|
+ "INSERT INTO lobster_sop_execution(company_id, instance_id, sop_id, sop_name, status, create_time) " +
|
|
|
+ "VALUES(%d, %d, '%s', '%s', 'triggered', NOW())",
|
|
|
+ context.getCompanyId(),
|
|
|
+ context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
|
|
|
+ sqlEscape(sopId != null ? sopId : ""),
|
|
|
+ sqlEscape(sopName)));
|
|
|
+ outputs.put("sopExecuted", true);
|
|
|
+ }
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("SOP执行失败: " + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private NodeExecutionResult handleCidTaskNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
- Map<String, Object> outputs = new HashMap<>();
|
|
|
- outputs.put("cidTaskCreated", true);
|
|
|
- return NodeExecutionResult.success(outputs);
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ String taskTemplate = config != null ? config.getString("taskTemplate") : null;
|
|
|
+ String taskName = config != null ? config.getString("taskName") : "cid_task";
|
|
|
+ outputs.put("taskTemplate", taskTemplate);
|
|
|
+ outputs.put("taskName", taskName);
|
|
|
+ if (config != null && (config.containsKey("apiUrl") || config.containsKey("url"))) {
|
|
|
+ outputs.putAll(invokeHttpFromConfig(config, context, "apiUrl", "url"));
|
|
|
+ } else if (auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ auxMapper.update(String.format(
|
|
|
+ "INSERT INTO lobster_cid_task(company_id, instance_id, customer_id, task_template, task_name, status, create_time) " +
|
|
|
+ "VALUES(%d, %d, '%s', '%s', '%s', 'created', NOW())",
|
|
|
+ context.getCompanyId(),
|
|
|
+ context.getWorkflowInstanceId() != null ? context.getWorkflowInstanceId() : 0,
|
|
|
+ sqlEscape(context.getCustomerId() != null ? String.valueOf(context.getCustomerId()) : ""),
|
|
|
+ sqlEscape(taskTemplate != null ? taskTemplate : ""),
|
|
|
+ sqlEscape(taskName)));
|
|
|
+ outputs.put("cidTaskCreated", true);
|
|
|
+ }
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("CID任务创建失败: " + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private NodeExecutionResult handleProductPushNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
- NodeExecutionResult result = NodeExecutionResult.success();
|
|
|
- result.setMessageToSend("推荐商品链接");
|
|
|
- return result;
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String productName = config != null ? config.getString("productName") : null;
|
|
|
+ String productUrl = config != null ? config.getString("productUrl") : null;
|
|
|
+ String productId = config != null ? config.getString("productId") : null;
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ if (productId != null && auxMapper != null && context.getCompanyId() != null) {
|
|
|
+ try {
|
|
|
+ long pid = Long.parseLong(productId.replaceAll("[^0-9]", ""));
|
|
|
+ List<Map<String, Object>> products = auxMapper.queryForList(
|
|
|
+ "SELECT product_name, product_url, price FROM lobster_product WHERE id="
|
|
|
+ + pid + " AND company_id=" + context.getCompanyId(),
|
|
|
+ context.getCompanyId());
|
|
|
+ if (!products.isEmpty()) {
|
|
|
+ Map<String, Object> p = products.get(0);
|
|
|
+ productName = p.get("product_name") != null ? p.get("product_name").toString() : productName;
|
|
|
+ productUrl = p.get("product_url") != null ? p.get("product_url").toString() : productUrl;
|
|
|
+ outputs.put("price", p.get("price"));
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("product lookup: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ if (productName == null) productName = "精选商品";
|
|
|
+ if (productUrl == null) productUrl = config != null ? config.getString("fallbackUrl") : "";
|
|
|
+ outputs.put("productName", productName);
|
|
|
+ outputs.put("productUrl", productUrl);
|
|
|
+ String msg = "为您推荐:" + productName;
|
|
|
+ if (productUrl != null && !productUrl.isEmpty()) msg += "\n" + productUrl;
|
|
|
+ NodeExecutionResult result = NodeExecutionResult.success(outputs);
|
|
|
+ result.setMessageToSend(msg);
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("商品推送失败: " + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private NodeExecutionResult handleLogisticsNotifyNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
- NodeExecutionResult result = NodeExecutionResult.success();
|
|
|
- result.setMessageToSend("您的订单已发货,物流单号:1234567890");
|
|
|
- return result;
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ String trackingNo = config != null ? config.getString("trackingNo") : null;
|
|
|
+ String carrier = config != null ? config.getString("carrier") : "快递";
|
|
|
+ if (trackingNo == null && context.getVariables() != null) {
|
|
|
+ Object v = context.getVariables().get("trackingNo");
|
|
|
+ if (v == null) v = context.getVariables().get("logisticsNo");
|
|
|
+ if (v != null) trackingNo = v.toString();
|
|
|
+ }
|
|
|
+ if (trackingNo == null && auxMapper != null && context.getCustomerId() != null) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> orders = auxMapper.queryForList(
|
|
|
+ "SELECT tracking_no, carrier FROM customer_order WHERE customer_id='"
|
|
|
+ + sqlEscape(String.valueOf(context.getCustomerId())) + "' AND company_id="
|
|
|
+ + context.getCompanyId() + " ORDER BY order_time DESC LIMIT 1",
|
|
|
+ context.getCompanyId());
|
|
|
+ if (!orders.isEmpty()) {
|
|
|
+ trackingNo = orders.get(0).get("tracking_no") != null
|
|
|
+ ? orders.get(0).get("tracking_no").toString() : trackingNo;
|
|
|
+ if (orders.get(0).get("carrier") != null) {
|
|
|
+ carrier = orders.get(0).get("carrier").toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) { logger.debug("logistics lookup: {}", e.getMessage()); }
|
|
|
+ }
|
|
|
+ if (trackingNo == null) trackingNo = "待更新";
|
|
|
+ outputs.put("trackingNo", trackingNo);
|
|
|
+ outputs.put("carrier", carrier);
|
|
|
+ NodeExecutionResult result = NodeExecutionResult.success(outputs);
|
|
|
+ result.setMessageToSend("您的订单已由" + carrier + "发出,物流单号:" + trackingNo);
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("物流通知失败: " + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private NodeExecutionResult handleExternalApiNode(int nodeType, String nodeConfig, ExecutionContext context) {
|
|
|
+ try {
|
|
|
+ JSONObject config = parseConfig(nodeConfig);
|
|
|
+ String apiCode = config != null ? config.getString("apiCode") : null;
|
|
|
+ Map<String, Object> outputs = new HashMap<>();
|
|
|
+ if (apiCode != null && smartApiMapper != null) {
|
|
|
+ Map<String, Object> api = smartApiMapper.selectByCode(apiCode);
|
|
|
+ if (api != null) {
|
|
|
+ JSONObject apiConfig = new JSONObject();
|
|
|
+ apiConfig.put("url", api.get("api_url"));
|
|
|
+ apiConfig.put("method", api.get("api_method"));
|
|
|
+ apiConfig.put("headers", api.get("headers_json"));
|
|
|
+ apiConfig.put("body", api.get("body_template"));
|
|
|
+ outputs.putAll(invokeHttpFromConfig(apiConfig, context, "url", "apiUrl"));
|
|
|
+ outputs.put("apiCode", apiCode);
|
|
|
+ outputs.put("apiCalled", true);
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ outputs.putAll(invokeHttpFromConfig(config, context, "url", "apiUrl"));
|
|
|
+ outputs.put("apiCalled", true);
|
|
|
+ return NodeExecutionResult.success(outputs);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return NodeExecutionResult.fail("外部API调用失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 从节点配置发起 HTTP 调用,支持变量替换 */
|
|
|
+ private Map<String, Object> invokeHttpFromConfig(JSONObject config, ExecutionContext context,
|
|
|
+ String... urlKeys) throws Exception {
|
|
|
Map<String, Object> outputs = new HashMap<>();
|
|
|
- outputs.put("apiCalled", true);
|
|
|
- return NodeExecutionResult.success(outputs);
|
|
|
+ if (config == null) return outputs;
|
|
|
+ String url = null;
|
|
|
+ for (String key : urlKeys) {
|
|
|
+ url = config.getString(key);
|
|
|
+ if (url != null && !url.isEmpty()) break;
|
|
|
+ }
|
|
|
+ if (url == null || url.isEmpty()) return outputs;
|
|
|
+ url = substituteVariables(url, context);
|
|
|
+ String method = config.getString("method");
|
|
|
+ if (method == null) method = config.getString("apiMethod");
|
|
|
+ if (method == null) method = "POST";
|
|
|
+
|
|
|
+ HttpHeaders httpHeaders = new HttpHeaders();
|
|
|
+ httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
|
|
+ Object headersObj = config.get("headers");
|
|
|
+ if (headersObj instanceof String && !((String) headersObj).isEmpty()) {
|
|
|
+ JSONObject hdr = JSON.parseObject((String) headersObj);
|
|
|
+ for (String key : hdr.keySet()) {
|
|
|
+ httpHeaders.set(key, substituteVariables(hdr.getString(key), context));
|
|
|
+ }
|
|
|
+ } else if (headersObj instanceof JSONObject) {
|
|
|
+ JSONObject hdr = (JSONObject) headersObj;
|
|
|
+ for (String key : hdr.keySet()) {
|
|
|
+ httpHeaders.set(key, substituteVariables(hdr.getString(key), context));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String body = config.getString("body");
|
|
|
+ if (body == null) body = config.getString("bodyTemplate");
|
|
|
+ if (body == null) body = config.getString("payload");
|
|
|
+ if (body == null) body = "{}";
|
|
|
+ body = substituteVariables(body, context);
|
|
|
+
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(body, httpHeaders);
|
|
|
+ HttpMethod httpMethod = "GET".equalsIgnoreCase(method) ? HttpMethod.GET : HttpMethod.POST;
|
|
|
+ ResponseEntity<String> resp = restTemplate.exchange(url, httpMethod, entity, String.class);
|
|
|
+ outputs.put("httpStatus", resp.getStatusCodeValue());
|
|
|
+ outputs.put("responseBody", resp.getBody());
|
|
|
+ outputs.put("requestUrl", url);
|
|
|
+ return outputs;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String substituteVariables(String text, ExecutionContext context) {
|
|
|
+ if (text == null) return null;
|
|
|
+ String result = text;
|
|
|
+ if (context.getVariables() != null) {
|
|
|
+ for (Map.Entry<String, Object> entry : context.getVariables().entrySet()) {
|
|
|
+ result = result.replace("${" + entry.getKey() + "}",
|
|
|
+ entry.getValue() != null ? entry.getValue().toString() : "");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (context.getCustomerId() != null) {
|
|
|
+ result = result.replace("${customerId}", String.valueOf(context.getCustomerId()));
|
|
|
+ }
|
|
|
+ if (context.getLastMessage() != null) {
|
|
|
+ result = result.replace("${lastMessage}", context.getLastMessage());
|
|
|
+ }
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
private boolean evaluateCondition(String condition, Map<String, Object> variables) {
|