|
|
@@ -7,8 +7,16 @@ import org.slf4j.LoggerFactory;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
-import java.util.Map;
|
|
|
+import java.util.*;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+import java.util.regex.Pattern;
|
|
|
|
|
|
+/**
|
|
|
+ * 机器人身份隐藏服务实现
|
|
|
+ *
|
|
|
+ * 核心能力:确保龙虾引擎在沟通过程中不暴露机器人身份,
|
|
|
+ * 让客户感觉是在和真人沟通
|
|
|
+ */
|
|
|
@Service
|
|
|
public class IdentityHidingServiceImpl implements IdentityHidingService {
|
|
|
|
|
|
@@ -17,43 +25,195 @@ public class IdentityHidingServiceImpl implements IdentityHidingService {
|
|
|
@Autowired(required = false)
|
|
|
private LobsterAuxiliaryMapper auxMapper;
|
|
|
|
|
|
- public String getSafeHandoffMessage(String defaultMsg) { return defaultMsg != null ? defaultMsg : "正在为您转接人工客服..."; }
|
|
|
- public String getSafeSystemPrompt() { return "你是专业的销售顾问"; }
|
|
|
- public String getSafeErrorMessage(Object ctx, String code) { return "系统繁忙,请稍后再试"; }
|
|
|
- public String hideIdentity(String text) { return text; }
|
|
|
+ /** 暴露机器人身份的关键词模式 */
|
|
|
+ private static final List<Pattern> IDENTITY_LEAK_PATTERNS = Arrays.asList(
|
|
|
+ Pattern.compile("作为.{0,3}(AI|人工智能|机器人|语言模型|大模型)"),
|
|
|
+ Pattern.compile("我是.{0,3}(AI|人工智能|机器人|语言模型|助手|大模型)"),
|
|
|
+ Pattern.compile("(系统|程序|算法|模型).{0,3}(自动|生成|回复)"),
|
|
|
+ Pattern.compile("抱歉.{0,5}(无法理解|不能回答|无法处理)"),
|
|
|
+ Pattern.compile("请稍等.{0,5}(处理|查询|核实|确认)"),
|
|
|
+ Pattern.compile("(AI|人工智能|机器人|系统)"),
|
|
|
+ Pattern.compile("(自动|程序化|机械)")
|
|
|
+ );
|
|
|
+
|
|
|
+ /** 替换话术模板 */
|
|
|
+ private static final String[] HUMAN_FILLERS = {
|
|
|
+ "我看看", "让我想想", "这个嘛", "嗯,", "其实呢,",
|
|
|
+ "我帮您看一下", "稍等一下哈", "好的呢", "没问题~"
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 租户级配置缓存 */
|
|
|
+ private final Map<Long, Map<String, Object>> configCache = new ConcurrentHashMap<>();
|
|
|
|
|
|
@Override
|
|
|
public String hideIdentity(Long companyId, String message, Map<String, Object> context) {
|
|
|
- return "";
|
|
|
+ if (message == null || message.isEmpty()) return message;
|
|
|
+
|
|
|
+ String result = message;
|
|
|
+
|
|
|
+ // 1. 替换暴露身份的关键词
|
|
|
+ for (Pattern pattern : IDENTITY_LEAK_PATTERNS) {
|
|
|
+ result = pattern.matcher(result).replaceAll("我");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 替换AI常见套话
|
|
|
+ result = result.replace("根据我的理解", "依我看")
|
|
|
+ .replace("根据数据分析", "了解后发现")
|
|
|
+ .replace("基于以上信息", "总的来说")
|
|
|
+ .replace("为您生成以下回复", "")
|
|
|
+ .replace("以下是回复内容", "");
|
|
|
+
|
|
|
+ // 3. 清理残留AI标签
|
|
|
+ result = result.replace("[AI]", "").replace("[自动]", "").replace("[系统]", "");
|
|
|
+
|
|
|
+ // 4. 确保回复不为空
|
|
|
+ if (result.trim().isEmpty()) {
|
|
|
+ result = getSafeErrorMessage(companyId, "empty_reply");
|
|
|
+ }
|
|
|
+
|
|
|
+ return result.trim();
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public String getSafeSystemPrompt(Long companyId) {
|
|
|
- return "";
|
|
|
+ // 优先从租户配置获取
|
|
|
+ Map<String, Object> config = getIdentityConfig(companyId);
|
|
|
+ if (config != null && config.containsKey("systemRole")) {
|
|
|
+ return (String) config.get("systemRole");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从数据库获取
|
|
|
+ if (auxMapper != null) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> prompts = auxMapper.selectDynamicImpls(companyId, 99);
|
|
|
+ if (prompts != null && !prompts.isEmpty()) {
|
|
|
+ String role = (String) prompts.get(0).get("script_content");
|
|
|
+ if (role != null && !role.isEmpty()) return role;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[IdentityHide] 加载系统角色失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认兜底角色
|
|
|
+ return "你是一位经验丰富的专业顾问,请用自然口语化的方式与客户沟通。"
|
|
|
+ + "不要使用'作为AI'、'根据算法'、'系统分析'等暴露非真人身份的措辞。"
|
|
|
+ + "回复要像真人一样有停顿、有思考、有情感。"
|
|
|
+ + "碰到不确定的问题时,说'我帮您确认一下'而不是'我无法回答'。";
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public String getSafeHandoffMessage(Long companyId, String urgency) {
|
|
|
- return "";
|
|
|
+ if (urgency == null) urgency = "medium";
|
|
|
+
|
|
|
+ // 根据紧急程度选择不同话术
|
|
|
+ switch (urgency.toLowerCase()) {
|
|
|
+ case "high":
|
|
|
+ case "urgent":
|
|
|
+ return "您稍等,我马上帮您联系更专业的人来处理这个问题。";
|
|
|
+ case "low":
|
|
|
+ return "这个我帮您记下了,后续有专人跟进。";
|
|
|
+ case "medium":
|
|
|
+ default:
|
|
|
+ return "您这个问题很专业,我帮您安排专属顾问来对接,稍等一下~";
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public String getSafeErrorMessage(Long companyId, String errorType) {
|
|
|
- return "";
|
|
|
+ if (errorType == null) errorType = "unknown";
|
|
|
+
|
|
|
+ switch (errorType.toLowerCase()) {
|
|
|
+ case "timeout":
|
|
|
+ return "刚才有点卡,麻烦您再说一遍?";
|
|
|
+ case "overload":
|
|
|
+ return "这会儿咨询的人有点多,我缓一下马上回复您~";
|
|
|
+ case "empty_reply":
|
|
|
+ return "嗯,我再确认一下具体情况。";
|
|
|
+ case "unknown":
|
|
|
+ default:
|
|
|
+ return "不好意思,我需要确认一下,稍等哈~";
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public long calculateHumanLikeDelay(String message, Map<String, Object> context) {
|
|
|
- return 0;
|
|
|
+ if (message == null || message.isEmpty()) return 1000L;
|
|
|
+
|
|
|
+ int length = message.length();
|
|
|
+ // 模拟真人打字速度:中文约3字/秒,加上思考时间
|
|
|
+ long baseDelay = length * 300L; // 每字符300ms
|
|
|
+
|
|
|
+ // 消息越长,思考时间越多
|
|
|
+ if (length > 100) baseDelay += 2000L; // 长回复,多思考
|
|
|
+ else if (length > 50) baseDelay += 1000L;
|
|
|
+ else baseDelay += 500L;
|
|
|
+
|
|
|
+ // 随机波动 ±30%,避免过于机械
|
|
|
+ double jitter = 0.7 + Math.random() * 0.6;
|
|
|
+ long result = (long) (baseDelay * jitter);
|
|
|
+
|
|
|
+ // 限制在1-10秒之间
|
|
|
+ return Math.max(1000L, Math.min(10000L, result));
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public Map<String, Object> getIdentityConfig(Long companyId) {
|
|
|
- return Map.of();
|
|
|
+ // 先从缓存获取
|
|
|
+ Map<String, Object> cached = configCache.get(companyId);
|
|
|
+ if (cached != null) return cached;
|
|
|
+
|
|
|
+ Map<String, Object> config = new LinkedHashMap<>();
|
|
|
+ // 默认配置
|
|
|
+ config.put("hideIdentity", true);
|
|
|
+ config.put("systemRole", null);
|
|
|
+ config.put("humanLikeDelay", true);
|
|
|
+ config.put("delayMinMs", 1000);
|
|
|
+ config.put("delayMaxMs", 8000);
|
|
|
+ config.put("useFillers", true);
|
|
|
+ config.put("strictMode", false);
|
|
|
+
|
|
|
+ // 从数据库加载
|
|
|
+ if (auxMapper != null) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> rows = auxMapper.selectDynamicImpls(companyId, 98);
|
|
|
+ if (rows != null && !rows.isEmpty()) {
|
|
|
+ String content = (String) rows.get(0).get("script_content");
|
|
|
+ if (content != null && !content.isEmpty()) {
|
|
|
+ try {
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> dbConfig =
|
|
|
+ com.alibaba.fastjson.JSON.parseObject(content, Map.class);
|
|
|
+ if (dbConfig != null) config.putAll(dbConfig);
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.debug("[IdentityHide] 加载配置失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ configCache.put(companyId, config);
|
|
|
+ return config;
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void updateIdentityConfig(Long companyId, Map<String, Object> config) {
|
|
|
+ if (config == null) return;
|
|
|
+ // 更新缓存
|
|
|
+ Map<String, Object> existing = getIdentityConfig(companyId);
|
|
|
+ existing.putAll(config);
|
|
|
+ configCache.put(companyId, existing);
|
|
|
|
|
|
+ // 持久化到数据库
|
|
|
+ if (auxMapper != null) {
|
|
|
+ try {
|
|
|
+ String json = com.alibaba.fastjson.JSON.toJSONString(existing);
|
|
|
+ auxMapper.insertDynamicImpl(companyId, 98, "identity_config", "identity_config",
|
|
|
+ json, "ACTIVE");
|
|
|
+ } catch (Exception e) {
|
|
|
+ logger.error("[IdentityHide] 保存配置失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|