package com.fs.company.service.workflow.execution; import com.fs.company.service.workflow.QualityScoringService; 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; /** * 统一出站前 8 维质量评分:AI 生成 / fallback / 沉默唤醒等必须评分后才可发送;租户配置话术跳过。 */ @Service public class LobsterOutboundQualityGate { private static final Logger log = LoggerFactory.getLogger(LobsterOutboundQualityGate.class); public static final String VAR_SKIP_QUALITY_SCORE = "_skipQualityScore"; public static final String VAR_QUALITY_SCORED = "_qualityScoredAtOutbound"; public static final String VAR_OUTBOUND_SOURCE = "_outboundSource"; @Autowired(required = false) private QualityScoringService qualityScoringService; @Value("${lobster.outbound.quality-gate.enabled:true}") private boolean enabled; public static void markTenantConfiguredScript(Map variables) { if (variables == null) { return; } variables.put(VAR_SKIP_QUALITY_SCORE, Boolean.TRUE); variables.put(VAR_OUTBOUND_SOURCE, "tenant_script"); } public static void markAiGenerated(Map variables, String source) { if (variables == null) { return; } variables.remove(VAR_SKIP_QUALITY_SCORE); variables.remove(VAR_QUALITY_SCORED); if (source != null && !source.isBlank()) { variables.put(VAR_OUTBOUND_SOURCE, source); } } public static void markScoredInEvolution(Map variables) { if (variables == null) { return; } variables.put(VAR_QUALITY_SCORED, Boolean.TRUE); } public static boolean shouldSkip(Map variables) { if (variables == null) { return false; } if (Boolean.TRUE.equals(variables.get(VAR_SKIP_QUALITY_SCORE))) { return true; } return Boolean.TRUE.equals(variables.get(VAR_QUALITY_SCORED)); } /** * @return 评分通过后的正文;null 表示拦截不发 */ public String applyBeforeDeliver(Long companyId, String message, Map variables) { if (message == null || message.isBlank()) { return message; } if (!enabled || shouldSkip(variables)) { return message; } if (LobsterMessageTemplateRenderer.isInternalInstructionText(message)) { log.warn("[OutboundQualityGate] block internal instruction text companyId={}", companyId); return null; } if (qualityScoringService == null) { log.warn("[OutboundQualityGate] QualityScoringService unavailable, block AI outbound companyId={}", companyId); return null; } String userQuestion = resolveUserQuestion(variables); String knowledgeBase = variables.get("_uddWebResearch") != null ? variables.get("_uddWebResearch").toString() : ""; String goal = variables.get("_lastOutboundNodeCode") != null ? variables.get("_lastOutboundNodeCode").toString() : (variables.get("_greetNodeCode") != null ? variables.get("_greetNodeCode").toString() : ""); String templateHint = variables.get("nodeMessageTemplate") != null ? variables.get("nodeMessageTemplate").toString() : ""; String source = variables.get(VAR_OUTBOUND_SOURCE) != null ? variables.get(VAR_OUTBOUND_SOURCE).toString() : "outbound"; try { QualityScoringService.ScoringResult scoring = qualityScoringService.scoreWithRetry( companyId, message, userQuestion, knowledgeBase, goal, templateHint, source, null); if (scoring == null) { log.warn("[OutboundQualityGate] scoring null, block companyId={}", companyId); return null; } String finalContent = scoring.getFinalContent() != null ? scoring.getFinalContent().trim() : ""; if (finalContent.isEmpty()) { log.warn("[OutboundQualityGate] empty after scoring, block companyId={}", companyId); return null; } if (LobsterMessageTemplateRenderer.isInternalInstructionText(finalContent)) { log.warn("[OutboundQualityGate] block instruction-like content after scoring companyId={}", companyId); return null; } if (!scoring.isFinalPassed()) { log.warn("[OutboundQualityGate] scoring not passed companyId={} reason={} score={}", companyId, scoring.getReason(), scoring.getFirstScore()); return null; } if (variables != null) { variables.put(VAR_QUALITY_SCORED, Boolean.TRUE); variables.put("_lastOutboundQualityScore", scoring.getFirstScore()); } return finalContent; } catch (Exception e) { log.warn("[OutboundQualityGate] scoring failed, block companyId={}: {}", companyId, e.getMessage()); return null; } } private static String resolveUserQuestion(Map variables) { if (variables == null) { return ""; } for (String key : new String[]{"lastReply", "customerMessage", "_silenceWakeCustomerMessage"}) { Object val = variables.get(key); if (val != null && !val.toString().isBlank()) { String text = val.toString().trim(); if (text.startsWith("[silence_wakeup]") || text.startsWith("[wait_wake]")) { return ""; } return text; } } return ""; } }