package com.fs.company.service.workflow.execution; import com.fs.company.domain.CompanyWorkflowLobsterNode; import com.fs.company.service.llm.MultiModelRouter; import com.fs.company.service.workflow.VariableSubstitutionEngine; import com.fs.company.service.workflow.prompt.EnginePromptHelper; import com.fs.company.service.workflow.prompt.LobsterPromptGuardrails; import com.fs.company.service.workflow.prompt.LobsterEnginePromptCatalog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Map; /** * 主动问候:客户未开口时 START/GREET 节点优先租户配置话术,否则 100% AI 生成;含 cooldown + 去重。 */ @Service public class LobsterProactiveGreetingService { private static final Logger log = LoggerFactory.getLogger(LobsterProactiveGreetingService.class); static final String VAR_LAST_GREET_MS = "_lastProactiveGreetMs"; static final String VAR_GREET_COUNT = "_proactiveGreetCount"; static final String VAR_LAST_GREET_TEXT = "_lastProactiveGreetText"; @Autowired(required = false) private MultiModelRouter multiModelRouter; @Autowired(required = false) private EnginePromptHelper enginePrompts; @Autowired(required = false) private VariableSubstitutionEngine varEngine; @Value("${lobster.proactive-greet.cooldown-hours:24}") private int cooldownHours; @Value("${lobster.proactive-greet.max-without-reply:2}") private int maxWithoutReply; @Value("${lobster.proactive-greet.ai-enabled:true}") private boolean aiEnabled; public boolean isProactiveContext(Map variables) { if (variables == null) { return false; } if (Boolean.TRUE.equals(variables.get("_proactiveOutbound"))) { return true; } Object trigger = variables.get("triggerSource"); if (trigger != null) { String src = trigger.toString(); return "proactive_task".equals(src) || "binding_provision".equals(src) || "scheduler".equals(src) || "tag_bind_or_hook".equals(src); } return false; } public boolean isOpeningGreetNode(CompanyWorkflowLobsterNode node) { if (node == null) { return false; } if (node.getNodeType() != null && node.getNodeType() == 1) { return true; } String code = node.getNodeCode(); return code != null && ("START".equalsIgnoreCase(code) || "GREET".equalsIgnoreCase(code)); } /** * START/GREET 开场:有配置话术则渲染后投递(可 AI 变体),无配置则 100% AI 生成。 */ public String resolveOpeningGreeting(Long companyId, Long instanceId, CompanyWorkflowLobsterNode node, Map variables) { if (LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) { return ""; } String configuredRaw = LobsterMessageTemplateRenderer.extractConfiguredOpeningScript(node, variables); if (!configuredRaw.isEmpty()) { String rendered = LobsterMessageTemplateRenderer.render(configuredRaw, variables, varEngine); log.debug("[ProactiveGreet] opening use configured script, node={}, instanceId={}", node != null ? node.getNodeCode() : null, instanceId); return resolveConfiguredScriptDelivery(companyId, instanceId, variables, rendered); } log.debug("[ProactiveGreet] opening no script configured, use AI, node={}, instanceId={}", node != null ? node.getNodeCode() : null, instanceId); return resolveAiOpeningGreeting(companyId, instanceId, variables, ""); } /** * 非 START 开场节点:主动任务场景下对配置话术做 cooldown/去重处理。 */ public String resolveForDelivery(Long companyId, Long instanceId, CompanyWorkflowLobsterNode node, Map variables, String renderedTemplate, boolean openingNode) { if (renderedTemplate == null || renderedTemplate.isBlank()) { if (openingNode && !LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) { return resolveOpeningGreeting(companyId, instanceId, node, variables); } return renderedTemplate; } if (LobsterMessageTemplateRenderer.hasCustomerSpoken(variables)) { return renderedTemplate; } if (openingNode) { return resolveConfiguredScriptDelivery(companyId, instanceId, variables, renderedTemplate); } boolean proactive = isProactiveContext(variables); if (!proactive) { return renderedTemplate; } return resolveConfiguredScriptDelivery(companyId, instanceId, variables, renderedTemplate); } private String resolveAiOpeningGreeting(Long companyId, Long instanceId, Map variables, String templateHint) { if (!aiEnabled || multiModelRouter == null) { log.warn("[ProactiveGreet] opening AI disabled or router unavailable, instanceId={}", instanceId); return null; } int sentCount = greetCount(variables); long lastMs = lastGreetMs(variables); long now = System.currentTimeMillis(); long cooldownMs = Math.max(1, cooldownHours) * 3600_000L; if (sentCount >= maxWithoutReply && now - lastMs < cooldownMs) { log.info("[ProactiveGreet] opening skip: max {} without reply, instanceId={}", maxWithoutReply, instanceId); return null; } String lastText = lastGreetText(variables); String ai = generateAiGreeting(companyId, variables, templateHint, sentCount); if (ai == null || ai.isBlank()) { log.warn("[ProactiveGreet] opening AI empty, instanceId={}", instanceId); return null; } ai = ai.trim(); if (lastText != null && isSameGreeting(ai, lastText) && now - lastMs < cooldownMs) { log.info("[ProactiveGreet] opening AI duplicate within cooldown, instanceId={}", instanceId); return null; } recordSent(variables, ai, now); LobsterOutboundQualityGate.markAiGenerated(variables, "ai_opening"); return ai; } /** 配置话术投递:cooldown/去重;必要时 AI 变体,租户脚本跳过 quality gate。 */ private String resolveConfiguredScriptDelivery(Long companyId, Long instanceId, Map variables, String renderedTemplate) { int sentCount = greetCount(variables); long lastMs = lastGreetMs(variables); long now = System.currentTimeMillis(); long cooldownMs = Math.max(1, cooldownHours) * 3600_000L; if (sentCount >= maxWithoutReply && now - lastMs < cooldownMs) { log.info("[ProactiveGreet] skip: already sent {} without reply, instanceId={}", sentCount, instanceId); return null; } String lastText = lastGreetText(variables); if (lastText != null && isSameGreeting(lastText, renderedTemplate) && now - lastMs < cooldownMs) { if (aiEnabled && multiModelRouter != null) { String varied = generateAiGreeting(companyId, variables, renderedTemplate, sentCount); if (varied != null && !varied.isBlank() && !isSameGreeting(varied, lastText)) { recordSent(variables, varied.trim(), now); LobsterOutboundQualityGate.markAiGenerated(variables, "ai_opening_varied"); return varied.trim(); } } log.info("[ProactiveGreet] skip duplicate script within cooldown, instanceId={}", instanceId); return null; } recordSent(variables, renderedTemplate, now); LobsterOutboundQualityGate.markTenantConfiguredScript(variables); return renderedTemplate; } private String generateAiGreeting(Long companyId, Map variables, String templateHint, int priorCount) { try { Map brands = LobsterMessageTemplateRenderer.brandMapFromVariables(variables); String customerName = variables.get("customerName") != null ? variables.get("customerName").toString() : "您"; String nodeCode = variables.get("_greetNodeCode") != null ? variables.get("_greetNodeCode").toString() : "START"; String prompt = enginePrompts != null ? enginePrompts.content("proactive_silent_greeting", companyId, EnginePromptHelper.vars( "customerName", customerName, "companyShortName", brands.getOrDefault("companyShortName", "我们公司"), "advisorName", brands.getOrDefault("advisorName", "专属顾问"), "mainProduct", brands.getOrDefault("mainProduct", "产品服务"), "nodeCode", nodeCode, "referenceTemplate", templateHint != null ? templateHint : "", "priorCount", String.valueOf(priorCount))) : LobsterEnginePromptCatalog.defaultContent("proactive_silent_greeting"); String reply = multiModelRouter.generateResponse(prompt, null, "proactive_silent_greeting"); return sanitizeAiGreeting(reply); } catch (Exception e) { log.debug("[ProactiveGreet] AI greeting failed: {}", e.getMessage()); return null; } } private static String sanitizeAiGreeting(String reply) { if (reply == null || reply.isBlank()) { return null; } String trimmed = reply.trim(); if (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length() > 2) { trimmed = trimmed.substring(1, trimmed.length() - 1); } trimmed = LobsterPromptGuardrails.finalizeCustomerFacingReply(trimmed, trimmed); return trimmed != null && !trimmed.isBlank() ? trimmed.trim() : null; } private static void recordSent(Map variables, String message, long now) { if (variables == null) { return; } variables.put(VAR_LAST_GREET_MS, now); variables.put(VAR_LAST_GREET_TEXT, message); variables.put(VAR_GREET_COUNT, greetCount(variables) + 1); } private static int greetCount(Map variables) { if (variables == null) { return 0; } Object raw = variables.get(VAR_GREET_COUNT); return raw instanceof Number ? ((Number) raw).intValue() : 0; } private static long lastGreetMs(Map variables) { if (variables == null) { return 0L; } Object raw = variables.get(VAR_LAST_GREET_MS); return raw instanceof Number ? ((Number) raw).longValue() : 0L; } private static String lastGreetText(Map variables) { if (variables == null) { return null; } Object raw = variables.get(VAR_LAST_GREET_TEXT); return raw != null ? raw.toString() : null; } private static boolean isSameGreeting(String a, String b) { if (a == null || b == null) { return false; } return normalize(a).equals(normalize(b)); } private static String normalize(String text) { return text.replaceAll("\\s+", "") .replace("${customerName}", "您") .replace("${customer_name}", "您"); } }