云联一号 пре 3 дана
родитељ
комит
e400fd71e9
56 измењених фајлова са 1086 додато и 568 уклоњено
  1. 1 1
      fs-agent/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java
  2. 1 1
      fs-framework/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java
  3. 47 47
      fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java
  4. 1 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java
  5. 3 14
      fs-service/src/main/java/com/fs/company/service/workflow/api/SmartApiCallNodeExecutor.java
  6. 7 3
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/AppImMessageChannel.java
  7. 5 1
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinDmMessageChannel.java
  8. 5 1
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinEcMessageChannel.java
  9. 5 1
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/JdMessageChannel.java
  10. 7 3
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/KuaishouDmMessageChannel.java
  11. 7 3
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/LineMessageChannel.java
  12. 7 3
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TelegramMessageChannel.java
  13. 5 1
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TmallMessageChannel.java
  14. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/WhatsAppMessageChannel.java
  15. 5 1
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/XiaohongshuDmMessageChannel.java
  16. 5 0
      fs-service/src/main/java/com/fs/company/service/workflow/contact/ContactInfo.java
  17. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/AppImContactAdapter.java
  18. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/DouyinDmContactAdapter.java
  19. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/DouyinEcContactAdapter.java
  20. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/ImContactAdapter.java
  21. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/JdContactAdapter.java
  22. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/KuaishouDmContactAdapter.java
  23. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/LineContactAdapter.java
  24. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/QwContactAdapter.java
  25. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/TelegramContactAdapter.java
  26. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/TmallContactAdapter.java
  27. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/WhatsAppContactAdapter.java
  28. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/WxContactAdapter.java
  29. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/XiaohongshuDmContactAdapter.java
  30. 15 0
      fs-service/src/main/java/com/fs/company/service/workflow/event/WorkflowPatcher.java
  31. 5 0
      fs-service/src/main/java/com/fs/company/service/workflow/evolution/UserNodeOptimizer.java
  32. 1 0
      fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/EvolutionSchedulerImpl.java
  33. 9 0
      fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/UserNodeOptimizerImpl.java
  34. 1 0
      fs-service/src/main/java/com/fs/company/service/workflow/feedback/impl/FeedbackDrivenEvolutionImpl.java
  35. 1 1
      fs-service/src/main/java/com/fs/company/service/workflow/impl/ContextAssemblerImpl.java
  36. 4 0
      fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeAdjusterImpl.java
  37. 40 38
      fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeExecutorImpl.java
  38. 112 20
      fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeImplServiceImpl.java
  39. 3 0
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterEvolutionEngineImpl.java
  40. 15 1
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterLearningCorpusServiceImpl.java
  41. 32 5
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterSalesCorpusServiceImpl.java
  42. 123 123
      fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterWorkflowExecutorImpl.java
  43. 58 57
      fs-service/src/main/java/com/fs/company/service/workflow/impl/MultiModelWorkflowGeneratorImpl.java
  44. 140 53
      fs-service/src/main/java/com/fs/company/service/workflow/impl/PendingAuditKnowledgeServiceImpl.java
  45. 2 1
      fs-service/src/main/java/com/fs/company/service/workflow/impl/PromptManagerImpl.java
  46. 38 38
      fs-service/src/main/java/com/fs/company/service/workflow/impl/SemanticTakeoverDetectorImpl.java
  47. 13 0
      fs-service/src/main/java/com/fs/company/service/workflow/impl/SensitiveWordServiceImpl.java
  48. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/impl/SummaryGeneratorImpl.java
  49. 103 17
      fs-service/src/main/java/com/fs/company/service/workflow/knowledge/impl/KnowledgeVersionManagerImpl.java
  50. 52 19
      fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/DistributedLearningServiceImpl.java
  51. 25 1
      fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/TenantLearningEngineImpl.java
  52. 2 0
      fs-service/src/main/java/com/fs/company/service/workflow/personalization/impl/PersonalizationEngineImpl.java
  53. 67 0
      fs-service/src/main/java/com/fs/company/service/workflow/queue/DeadLetterQueue.java
  54. 2 2
      fs-service/src/main/java/com/fs/company/service/workflow/scheduler/WorkflowTriggerScheduler.java
  55. 86 85
      fs-service/src/main/java/com/fs/company/service/workflow/semantic/impl/SemanticAnalyzerImpl.java
  56. 1 1
      fs-task/src/main/java/com/fs/task/TaskPackages.java

+ 1 - 1
fs-agent/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java

@@ -5,7 +5,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 
 /**
- * Controller ʹ��ȫ���������� fs-saasadmin ͬ����ͻ��Service ������Ĭ�϶�����
+ * Controller 使用全限定类名避免与 fs-saasadmin 同名冲突,Service 则使用默认短名称
  */
 public class HybridBeanNameGenerator extends AnnotationBeanNameGenerator {
 

+ 1 - 1
fs-framework/src/main/java/com/fs/framework/config/HybridBeanNameGenerator.java

@@ -5,7 +5,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
 import org.springframework.context.annotation.AnnotationBeanNameGenerator;
 
 /**
- * Controller ʹ��ȫ���������� fs-saasadmin ͬ����ͻ��Service ������Ĭ�϶�����
+ * Controller 使用全限定类名避免与 fs-saasadmin 同名冲突,Service 则使用默认短名称
  */
 public class HybridBeanNameGenerator extends AnnotationBeanNameGenerator {
 

+ 47 - 47
fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java

@@ -19,58 +19,58 @@ public class Finance {
     public native static long NewSdk();
 
     /**
-     * ��ʼ������
-     * Returnֵ=0��ʾ��API���óɹ�
+     * 初始化函数
+     * Return值=0表示该API调用成功
      *
-     * @param [in] sdk			NewSdk���ص�sdkָ��
-     * @param [in] corpid      ������ҵ����ҵid�����磺wwd08c8exxxx5ab44d����������ҵ΢�Ź����--�ҵ���ҵ--��ҵ��Ϣ�鿴
-     * @param [in] secret		�������ݴ浵��Secret����������ҵ΢�Ź����--������--�������ݴ浵�鿴
-     * @return �����Ƿ��ʼ���ɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
+     * @param [in] sdk			NewSdk返回的sdk指针
+     * @param [in] corpid      调用企业的企业id,例如:wwd08c8exxxx5ab44d,登录企业微信管理后台--我的企业--企业信息查看
+     * @param [in] secret		会话内容存档的Secret,登录企业微信管理后台--管理工具--会话内容存档查看
+     * @return 返回是否初始化成功
+     * 0   - 成功
+     * !=0 - 失败
      */
     public native static int Init(long sdk, String corpid, String secret);
 
     /**
-     * ��ȡ�����¼����
-     * Returnֵ=0��ʾ��API���óɹ�
+     * 拉取聊天记录函数
+     * Return值=0表示该API调用成功
      *
-     * @param [in]  sdk				NewSdk���ص�sdkָ��
-     * @param [in]  seq				��ָ����seq��ʼ��ȡ��Ϣ��ע����Ƿ��ص���Ϣ��seq+1��ʼ���أ�seqΪ֮ǰ�ӿڷ��ص����seqֵ���״�ʹ����ʹ��seq:0
-     * @param [in]  limit			һ����ȡ����Ϣ���������ֵ1000��������1000���᷵�ش���
-     * @param [in]  proxy			ʹ�ô����������Ҫ�����������ӡ��磺socks5://10.0.0.1:8081 ���� http://10.0.0.1:8081
-     * @param [in]  passwd			�����˺����룬��Ҫ���������˺����롣�� user_name:passwd_123
-     * @param [out] chatDatas		���ر�����ȡ��Ϣ�����ݣ�slice�ṹ��.���ݰ���errcode/errmsg���Լ�ÿ����Ϣ���ݡ�
-     * @return �����Ƿ���óɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
+     * @param [in]  sdk				NewSdk返回的sdk指针
+     * @param [in]  seq				从指定的seq开始拉取消息,注意返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
+     * @param [in]  limit			一次拉取的消息条数,最大值1000,超过1000会返回错误
+     * @param [in]  proxy			使用代理的请求,需要接入代理的URL。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
+     * @param [in]  passwd			代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
+     * @param [out] chatDatas		返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。
+     * @return 返回是否调用成功
+     * 0   - 成功
+     * !=0 - 失败
      */
     public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);
 
     /**
-     * ��ȡý����Ϣ����
-     * Returnֵ=0��ʾ��API���óɹ�
+     * 获取媒体消息数据
+     * Return值=0表示该API调用成功
      *
-     * @param [in]  sdk				NewSdk���ص�sdkָ��
-     * @param [in]  sdkFileid		��GetChatData���ص�������Ϣ�У�ý����Ϣ������sdkfileid
-     * @param [in]  proxy			ʹ�ô����������Ҫ�����������ӡ��磺socks5://10.0.0.1:8081 ���� http://10.0.0.1:8081
-     * @param [in]  passwd			�����˺����룬��Ҫ���������˺����롣�� user_name:passwd_123
-     * @param [in]  indexbuf		ý����Ϣ��Ƭ��ȡ����Ҫ����ÿ����ȡ��������Ϣ���״β���Ҫ��д��Ĭ����ȡ512k������ÿ�ε���ֻ��Ҫ���ϴε��÷��ص�outindexbuf���뼴�ɡ�
-     * @param [out] media_data		���ر�����ȡ��ý������.MediaData�ṹ��.���ݰ���data(��������)/outindexbuf(�´�����)/is_finish(��ȡ��ɱ��)
-     * @return �����Ƿ���óɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
+     * @param [in]  sdk				NewSdk返回的sdk指针
+     * @param [in]  sdkFileid		从GetChatData返回的聊天消息中,媒体消息包含的sdkfileid
+     * @param [in]  proxy			使用代理的请求,需要接入代理的URL。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
+     * @param [in]  passwd			代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
+     * @param [in]  indexbuf		媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf带入即可。
+     * @param [out] media_data		返回本次拉取的媒体数据.MediaData结构体.内容包括data(消息数据)/outindexbuf(下次索引)/is_finish(拉取完成标记)
+     * @return 返回是否调用成功
+     * 0   - 成功
+     * !=0 - 失败
      */
     public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
 
     /**
-     * @param [in]  encrypt_key, getchatdata���ص�encrypt_key
-     * @param [in]  encrypt_msg, getchatdata���ص�content
-     * @param [out] msg, ���ܵ���Ϣ����
-     * @return �����Ƿ���óɹ�
-     * 0   - �ɹ�
-     * !=0 - ʧ��
-     * @brief ��������
+     * @param [in]  encrypt_key, getchatdata返回的encrypt_key
+     * @param [in]  encrypt_msg, getchatdata返回的content
+     * @param [out] msg, 解密的消息明文
+     * @return 返回是否调用成功
+     * 0   - 成功
+     * !=0 - 失败
+     * @brief 数据解密
      */
     public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
 
@@ -79,20 +79,20 @@ public class Finance {
     public native static long NewSlice();
 
     /**
-     * @return
-     * @brief �ͷ�slice����NewSlice�ɶ�ʹ��
+     * @return 
+     * @brief 释放slice,和NewSlice成对使用
      */
     public native static void FreeSlice(long slice);
 
     /**
-     * @return ����
-     * @brief ��ȡslice����
+     * @return 内容
+     * @brief 获取slice内容
      */
     public native static String GetContentFromSlice(long slice);
 
     /**
-     * @return ����
-     * @brief ��ȡslice���ݳ���
+     * @return 长度
+     * @brief 获取slice数据长度
      */
     public native static int GetSliceLen(long slice);
 
@@ -102,13 +102,13 @@ public class Finance {
 
     /**
      * @return outindex
-     * @brief ��ȡmediadata outindex
+     * @brief 获取mediadata outindex
      */
     public native static String GetOutIndexBuf(long mediaData);
 
     /**
      * @return data
-     * @brief ��ȡmediadata data����
+     * @brief 获取mediadata data数据
      */
     public native static byte[] GetData(long mediaData);
 
@@ -117,8 +117,8 @@ public class Finance {
     public native static int GetDataLen(long mediaData);
 
     /**
-     * @return 1��ɡ�0δ���
-     * @brief �ж�mediadata�Ƿ����
+     * @return 1完成、0未完成
+     * @brief 判断mediadata是否结束
      */
     public native static int IsMediaDataFinish(long mediaData);
 

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java

@@ -437,7 +437,7 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
             CompanyKnowledgeBase param = new CompanyKnowledgeBase();
             param.setCompanyId(companyId);
             param.setQuestion(query);
-            List<CompanyKnowledgeBase> list = companyKnowledgeBaseMapper.selectCompanyKnowledgeBaseList(param);
+            List<CompanyKnowledgeBase> list = knowledgeBaseMapper.selectKnowledgeList(companyId, query, null, null, null);
             int n = Math.min(topK, list == null ? 0 : list.size());
             for (int i = 0; i < n; i++) {
                 CompanyKnowledgeBase kb = list.get(i);

+ 3 - 14
fs-service/src/main/java/com/fs/company/service/workflow/api/SmartApiCallNodeExecutor.java

@@ -126,19 +126,8 @@ public class SmartApiCallNodeExecutor {
         authHeaders.forEach(headers::set);
 
         String url = ep.baseUrl;
-        if ("GET".equalsIgnoreCase(ep.httpMethod)) {
-            StringBuilder qs = new StringBuilder();
-            for (Map.Entry<String, Object> e : params.entrySet()) {
-                if (qs.length() > 0) qs.append("&");
-                qs.append(e.getKey()).append("=").append(e.getValue());
-            }
-            if (qs.length() > 0) url += "?" + qs;
-            ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(headers), String.class);
-            return resp.getBody();
-        } else {
-            HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(params), headers);
-            ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);
-            return resp.getBody();
-        }
+        HttpEntity<String> entity = new HttpEntity<>(JSON.toJSONString(params), headers);
+        ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);
+        return resp.getBody();
     }
 }

+ 7 - 3
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/AppImMessageChannel.java

@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestTemplate;
 
 /**
- * 小红书私信消息通道 - 对接小红书开放平��? */
+ * APP IM消息通道 - 对接APP开放平台 */
 @Slf4j
 @Component
 public class AppImMessageChannel implements MessageChannel {
@@ -40,15 +40,19 @@ public class AppImMessageChannel implements MessageChannel {
                 HttpEntity<String> entity = new HttpEntity<>(body, headers);
                 restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
             }
-            log.info("[APP_IM] 消息发��? to={}", request.getChannelUserId());
+            log.info("[APP_IM] 消息发 to={}", request.getChannelUserId());
             return MessageChannelResult.ok(CHANNEL_TYPE, "appim_" + System.currentTimeMillis());
         } catch (Exception e) {
             return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 5 - 1
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinDmMessageChannel.java

@@ -49,8 +49,12 @@ public class DouyinDmMessageChannel implements MessageChannel {
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String getAccessToken(Long companyId) {

+ 5 - 1
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinEcMessageChannel.java

@@ -48,8 +48,12 @@ public class DouyinEcMessageChannel implements MessageChannel {
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 5 - 1
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/JdMessageChannel.java

@@ -49,8 +49,12 @@ public class JdMessageChannel implements MessageChannel {
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String getToken(Long companyId) {

+ 7 - 3
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/KuaishouDmMessageChannel.java

@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestTemplate;
 
 /**
- * 小红书私信消息通道 - 对接小红书开放平��? */
+ * 快手私信消息通道 - 对接快手开放平台 */
 @Slf4j
 @Component
 public class KuaishouDmMessageChannel implements MessageChannel {
@@ -40,15 +40,19 @@ public class KuaishouDmMessageChannel implements MessageChannel {
                 HttpEntity<String> entity = new HttpEntity<>(body, headers);
                 restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
             }
-            log.info("[KUAISHOU_DM] 消息发��? to={}", request.getChannelUserId());
+            log.info("[KUAISHOU_DM] 消息发 to={}", request.getChannelUserId());
             return MessageChannelResult.ok(CHANNEL_TYPE, "ks_" + System.currentTimeMillis());
         } catch (Exception e) {
             return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 7 - 3
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/LineMessageChannel.java

@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestTemplate;
 
 /**
- * 小红书私信消息通道 - 对接小红书开放平��? */
+ * Line消息通道 - 对接Line开放平台 */
 @Slf4j
 @Component
 public class LineMessageChannel implements MessageChannel {
@@ -40,15 +40,19 @@ public class LineMessageChannel implements MessageChannel {
                 HttpEntity<String> entity = new HttpEntity<>(body, headers);
                 restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
             }
-            log.info("[LINE] 消息发��? to={}", request.getChannelUserId());
+            log.info("[LINE] 消息发 to={}", request.getChannelUserId());
             return MessageChannelResult.ok(CHANNEL_TYPE, "line_" + System.currentTimeMillis());
         } catch (Exception e) {
             return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 7 - 3
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TelegramMessageChannel.java

@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestTemplate;
 
 /**
- * 小红书私信消息通道 - 对接小红书开放平��? */
+ * Telegram消息通道 - 对接Telegram开放平台 */
 @Slf4j
 @Component
 public class TelegramMessageChannel implements MessageChannel {
@@ -40,15 +40,19 @@ public class TelegramMessageChannel implements MessageChannel {
                 HttpEntity<String> entity = new HttpEntity<>(body, headers);
                 restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
             }
-            log.info("[TELEGRAM] 消息发��? to={}", request.getChannelUserId());
+            log.info("[TELEGRAM] 消息发 to={}", request.getChannelUserId());
             return MessageChannelResult.ok(CHANNEL_TYPE, "tg_" + System.currentTimeMillis());
         } catch (Exception e) {
             return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 5 - 1
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TmallMessageChannel.java

@@ -56,8 +56,12 @@ public class TmallMessageChannel implements MessageChannel {
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/WhatsAppMessageChannel.java

@@ -56,7 +56,7 @@ public class WhatsAppMessageChannel implements MessageChannel {
             String url = WA_BASE_URL + "/" + phoneNumberId + "/messages";
             JSONObject body = new JSONObject();
             body.put("messaging_product", "whatsapp");
-            body.put("to", request.getToUserId());
+            body.put("to", request.getChannelUserId());
             body.put("type", "text");
             JSONObject text = new JSONObject();
             text.put("body", request.getContent());
@@ -69,7 +69,7 @@ public class WhatsAppMessageChannel implements MessageChannel {
             HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
 
             ResponseEntity<String> resp = restTemplate.postForEntity(url, entity, String.class);
-            log.info("[WhatsApp] 发送完成: to={}, httpStatus={}, body={}", request.getToUserId(),
+            log.info("[WhatsApp] 发送完成: to={}, httpStatus={}, body={}", request.getChannelUserId(),
                     resp.getStatusCode(), resp.getBody());
 
             return MessageChannelResult.ok(CHANNEL_TYPE, "wa_msg_" + System.currentTimeMillis());

+ 5 - 1
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/XiaohongshuDmMessageChannel.java

@@ -48,8 +48,12 @@ public class XiaohongshuDmMessageChannel implements MessageChannel {
         }
     }
 
+    @Override public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
     @Override public boolean isAvailable(Long companyId) {
-        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).ready;
     }
 
     private String extractToken(String json) {

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/workflow/contact/ContactInfo.java

@@ -17,6 +17,9 @@ import java.util.Map;
  */
 public class ContactInfo {
 
+    /** 租户ID */
+    private Long companyId;
+
     /** 联系人数据库主键ID */
     private Long contactId;
 
@@ -47,6 +50,8 @@ public class ContactInfo {
         return info;
     }
 
+    public Long getCompanyId() { return companyId; }
+    public void setCompanyId(Long companyId) { this.companyId = companyId; }
     public Long getContactId() { return contactId; }
     public void setContactId(Long contactId) { this.contactId = contactId; }
     public String getChannelType() { return channelType; }

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/AppImContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * APP内IM联系人适配器
- * �? app_im_user (需建表或从 APP 用户体系同步)
+ * 源表: app_im_user (需建表或从 APP 用户体系同步)
  */
 @Service
 public class AppImContactAdapter implements ContactAdapter {

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/DouyinDmContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * 抖音私信联系人适配器
- * �? douyin_contact (需建表或从抖音开放平台同�?
+ * 源表: douyin_contact (需建表或从抖音开放平台同步)
  */
 @Service
 public class DouyinDmContactAdapter implements ContactAdapter {

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/DouyinEcContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * 抖音电商联系人适配器
- * �? douyin_ec_contact (需建表或从抖音电商开放平台同�?
+ * 源表: douyin_ec_contact (需建表或从抖音电商开放平台同步)
  */
 @Service
 public class DouyinEcContactAdapter implements ContactAdapter {

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/ImContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/JdContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * 京东联系人适配器
- * �? jd_contact (需建表或从京东开放平台同�?
+ * 源表: jd_contact (需建表或从京东开放平台同步)
  */
 @Service
 public class JdContactAdapter implements ContactAdapter {

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/KuaishouDmContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * 快手私信联系人适配器
- * �? kuaishou_contact (需建表或从快手开放平台同�?
+ * 源表: kuaishou_contact (需建表或从快手开放平台同步)
  */
 @Service
 public class KuaishouDmContactAdapter implements ContactAdapter {

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/LineContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * Line 联系人适配器
- * �? line_contact (需建表或从 Line Messaging API 同步)
+ * 源表: line_contact (需建表或从 Line Messaging API 同步)
  */
 @Service
 public class LineContactAdapter implements ContactAdapter {

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/QwContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/TelegramContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * Telegram 联系人适配器
- * �? telegram_contact (需建表或从 Telegram Bot API 同步)
+ * 源表: telegram_contact (需建表或从 Telegram Bot API 同步)
  */
 @Service
 public class TelegramContactAdapter implements ContactAdapter {

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/TmallContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/WhatsAppContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.mapper.WhatsAppContactMapper;
 import com.fs.company.service.workflow.contact.ContactAdapter;

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/WxContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/contact/impl/XiaohongshuDmContactAdapter.java

@@ -1,4 +1,4 @@
-package com.fs.company.service.workflow.contact.impl;
+package com.fs.company.service.workflow.contact.impl;
 
 import com.fs.company.service.workflow.contact.ContactAdapter;
 import com.fs.company.service.workflow.contact.ContactInfo;
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Service;
 
 /**
  * 小红书私信联系人适配器
- * �? xhs_contact (需建表或从小红书开放平台同�?
+ * 源表: xhs_contact (需建表或从小红书开放平台同步)
  */
 @Service
 public class XiaohongshuDmContactAdapter implements ContactAdapter {

+ 15 - 0
fs-service/src/main/java/com/fs/company/service/workflow/event/WorkflowPatcher.java

@@ -1,5 +1,6 @@
 package com.fs.company.service.workflow.event;
 
+import com.alibaba.fastjson.JSONObject;
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -28,4 +29,18 @@ public class WorkflowPatcher {
 
     public void applyPatch(Long id) { if (auxMapper != null) auxMapper.applyPatch(id); }
     public void rejectPatch(Long id) { if (auxMapper != null) auxMapper.rejectPatch(id); }
+
+    public boolean patchNode(Long instanceId, JSONObject newNode, String insertAt) {
+        logger.info("[WorkflowPatcher] patchNode: instanceId={}, insertAt={}", instanceId, insertAt);
+        if (auxMapper == null) return false;
+        try {
+            String nodeJson = newNode != null ? newNode.toJSONString() : "{}";
+            auxMapper.insertPatch(0L, "company_workflow_lobster_node", instanceId,
+                    "canvas_data", null, nodeJson, "patchNode insertAt=" + insertAt);
+            return true;
+        } catch (Exception e) {
+            logger.warn("[WorkflowPatcher] patchNode failed: {}", e.getMessage());
+            return false;
+        }
+    }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/workflow/evolution/UserNodeOptimizer.java

@@ -109,6 +109,11 @@ public interface UserNodeOptimizer {
      */
     Map<String, Object> getOptimizationStats(Long companyId);
 
+    /**
+     * 设置节点优化配置
+     */
+    void setOptimizationConfig(Long companyId, Long workflowId, String nodeCode, boolean enabled, boolean autoApply, String configBy);
+
     /**
      * 用户级节点优化记录
      */

+ 1 - 0
fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/EvolutionSchedulerImpl.java

@@ -1,6 +1,7 @@
 package com.fs.company.service.workflow.evolution.impl;
 
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.fs.company.service.workflow.feedback.impl.FeedbackDrivenEvolutionImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/workflow/evolution/impl/UserNodeOptimizerImpl.java

@@ -167,6 +167,15 @@ public class UserNodeOptimizerImpl implements UserNodeOptimizer {
         } catch (Exception e) { logger.error("[UserNodeOptimizer] 更新优化配置失败", e); }
     }
 
+    public Map<String, Object> getOptimizationConfig(Long companyId, Long workflowId, String nodeCode) {
+        Map<String, Object> config = new HashMap<>();
+        config.put("companyId", companyId);
+        config.put("workflowId", workflowId);
+        config.put("nodeCode", nodeCode);
+        config.put("enabled", isOptimizationEnabled(companyId, workflowId, nodeCode));
+        return config;
+    }
+
     private UserNodeOptimization mapRowToOptimization(Map<String, Object> row) {
         UserNodeOptimization opt = new UserNodeOptimization();
         opt.setId(row.get("id") != null ? ((Number) row.get("id")).longValue() : null);

+ 1 - 0
fs-service/src/main/java/com/fs/company/service/workflow/feedback/impl/FeedbackDrivenEvolutionImpl.java

@@ -1,6 +1,7 @@
 package com.fs.company.service.workflow.feedback.impl;
 
 import com.alibaba.fastjson.JSON;
+import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import com.fs.company.mapper.LobsterFeedbackMapper;
 import com.fs.company.service.llm.MultiModelRouter;
 import com.fs.company.service.workflow.feedback.*;

+ 1 - 1
fs-service/src/main/java/com/fs/company/service/workflow/impl/ContextAssemblerImpl.java

@@ -391,7 +391,7 @@ public class ContextAssemblerImpl implements ContextAssembler {
                     Map<String, Object> entry = new HashMap<>();
                     StringBuilder sb = new StringBuilder();
                     for (com.fs.company.service.workflow.vector.VectorPatternMatcher.VectorMatchResult vr : vecResults) {
-                        if (vr.getTextContent() != null) sb.append(vr.getTextContent()).append("\n");
+                        if (vr.getText() != null) sb.append(vr.getText()).append("\n");
                     }
                     entry.put("title", "向量语义匹配 Top" + vecResults.size());
                     entry.put("content", sb.toString());

+ 4 - 0
fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeAdjusterImpl.java

@@ -6,6 +6,10 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
 @Service
 public class DynamicNodeAdjusterImpl {
 

+ 40 - 38
fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeExecutorImpl.java

@@ -6,9 +6,11 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.company.service.llm.MultiModelRouter;
 import com.fs.company.service.workflow.DynamicNodeExecutor;
 import com.fs.company.service.workflow.LobsterNodeTypeService;
+import com.fs.company.service.workflow.QualityScoringService;
 import com.fs.company.service.workflow.channel.MessageChannelRequest;
 import com.fs.company.service.workflow.channel.MessageChannelResult;
 import com.fs.company.service.workflow.channel.MessageChannelRouter;
+import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import com.fs.company.domain.LobsterWorkflowNodeType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -134,7 +136,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             logger.info("[DynamicExecutor] Executing unknown node type {} with AI", nodeType);
             NodeExecutionResult aiResult = executeWithAI(nodeType, nodeConfig, context, typeInfo);
 
-            // 学习落库:把 AI 输出作为 DSL 候选保�?
+            // 学习落库:把 AI 输出作为 DSL 候选保
             if (dynamicNodeImplService != null && aiResult.isSuccess() && fingerprint != null) {
                 String dsl = buildDslFromAiResult(aiResult, nodeType, nodeConfig);
                 Long implId = dynamicNodeImplService.saveLearned(nodeType, fingerprint, dsl,
@@ -156,7 +158,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
     }
 
     /**
-     * 执行�?DSL:把 AI 拆解�?[{type, config, outputVar}, ...] 序列依次走已�?handler
+     * 执行子DSL:把 AI 拆解为 [{type, config, outputVar}, ...] 序列依次走已有 handler
      */
     private NodeExecutionResult executeSubDsl(String subDslJson, ExecutionContext context) {
         try {
@@ -196,7 +198,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
     }
 
     /**
-     * �?AI 一次�?JSON 结果包装�?DSL 候选(保留原配�?原始 messageToSend 作为 fallback�?
+     * 将 AI 一次性 JSON 结果包装为 DSL 候选(保留原配置、原始 messageToSend 作为 fallback)
      */
     private String buildDslFromAiResult(NodeExecutionResult r, int nodeType, String origConfig) {
         JSONObject dsl = new JSONObject();
@@ -230,7 +232,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                                               LobsterWorkflowNodeType typeInfo) {
         try {
             String nodeTypeName = typeInfo != null ? typeInfo.getNodeName() : "未知节点";
-            String nodeDesc = typeInfo != null ? typeInfo.getDescription() : "无描�?;
+            String nodeDesc = typeInfo != null ? typeInfo.getDescription() : "无描述";
 
             String prompt = "You are an intelligent workflow node executor. Execute the following workflow node.\n\n" +
                 "NODE_TYPE: " + nodeType + "\n" +
@@ -403,18 +405,18 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             Map<String, Object> outputs = new HashMap<>();
             long now = System.currentTimeMillis();
             if ("daily".equals(waitType)) {
-                // 每日定时(sendTime 格式 HH:mm:ss�?
+                // 每日定时(sendTime 格式 HH:mm:ss
                 String sendTime = config.getString("sendTime");
                 long next = computeNextDaily(sendTime != null ? sendTime : "08:00:00");
                 outputs.put("waitType", "daily");
                 outputs.put("nextTrigger", next);
                 outputs.put("waitUntil", next);
             } else if ("holiday".equals(waitType)) {
-                // 节假日触发:记录 holidayList,实际触发由调度器判�?
+                // 节假日触发:记录 holidayList,实际触发由调度器判
                 String holidayList = config.getString("holidayList");
                 outputs.put("waitType", "holiday");
                 outputs.put("holidayList", holidayList);
-                outputs.put("nextCheck", now + 3600_000L); // 每小时检查一�?
+                outputs.put("nextCheck", now + 3600_000L); // 每小时检查一
             } else if ("tillDate".equals(waitType)) {
                 // 等到指定日期
                 String dateField = config.getString("dateField");
@@ -470,7 +472,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         } catch (Exception e) { return System.currentTimeMillis() + 86_400_000L; }
     }
 
-    /** 计算下一个每月日触发时间�?*/
+    /** 计算下一个每月日触发时间*/
     private long computeNextMonthly(int dayOfMonth) {
         java.util.Calendar cal = java.util.Calendar.getInstance();
         cal.set(java.util.Calendar.DAY_OF_MONTH, Math.min(dayOfMonth, 28));
@@ -502,7 +504,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                     extra.put("openid", String.valueOf(context.getVariables().get("openid")));
                 }
                 Map<String, Object> orderResult = payService.createPayOrder(
-                    context.getCompanyId(), context.getCustomerId(), context.getInstanceId(),
+                    context.getCompanyId(), context.getCustomerId(), context.getWorkflowInstanceId(),
                     productName, amount, extra);
                 outputs.putAll(orderResult);
             } else {
@@ -542,12 +544,12 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                         amount != null ? Double.valueOf(amount) : 0, sqlEscape(desc)));
                 } catch (Exception e) { logger.warn("coupon record failed: {}", e.getMessage()); }
             }
-            String msg = desc.isEmpty() ? "为您准备了一份专属优�? : desc;
+            String msg = desc.isEmpty() ? "为您准备了一份专属优惠" : desc;
             NodeExecutionResult r = NodeExecutionResult.success(outputs);
             r.setMessageToSend(msg);
             return r;
         } catch (Exception e) {
-            return NodeExecutionResult.fail("优惠券节点处理失�? " + e.getMessage());
+            return NodeExecutionResult.fail("优惠券节点处理失败: " + e.getMessage());
         }
     }
 
@@ -560,7 +562,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             Map<String, Object> outputs = new HashMap<>();
             List<String> tagsResolved = new java.util.ArrayList<>();
             if (extractFields != null && auxMapper != null) {
-                // AI 提取标签:从客户上下文抽取字段�?
+                // AI 提取标签:从客户上下文抽取字段
                 String source = context.getVariables() != null ? JSON.toJSONString(context.getVariables()) : "";
                 if (context.getLastMessage() != null) source = context.getLastMessage() + " " + source;
                 try {
@@ -574,7 +576,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                             String key = t.getString("key");
                             String val = t.getString("value");
                             if (key != null && val != null) {
-                                // 写入 customer_tag �?
+                                // 写入 customer_tag 
                                 try {
                                     auxMapper.update(String.format(
                                         "INSERT INTO customer_tag(company_id, external_user_id, tag_key, tag_value, create_time) " +
@@ -614,8 +616,8 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             String prompt = "你是一位贴心的客户关怀顾问,需要给客户发送关怀消息。\n" +
                 "关怀类型: " + caretType + "\n" +
                 "客户信息: " + (context.getVariables() != null ? JSON.toJSONString(context.getVariables()) : "{}") + "\n" +
-                "最近消�? " + (context.getLastMessage() != null ? context.getLastMessage() : "") + "\n" +
-                "请生成一条自然、温暖、有温度的关怀消息�?0-80字。只用输出消息文本,不要引号�?;
+                "最近消息: " + (context.getLastMessage() != null ? context.getLastMessage() : "") + "\n" +
+                "请生成一条自然、温暖、有温度的关怀消息(20-80字)。只用输出消息文本,不要引号。";
             String careMsg = multiModelRouter != null
                 ? multiModelRouter.generateResponse(prompt, null, "care_generator")
                 : "祝您一切都好,有需要随时找我~";
@@ -639,22 +641,22 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             String prompt = "你是一位客户满意度调研顾问,请生成一条满意度调研消息。\n" +
                 "调研类型: " + surveyType + "\n" +
                 "客户信息: " + (context.getVariables() != null ? JSON.toJSONString(context.getVariables()) : "{}") + "\n" +
-                "请生成一条自然友好的调研消息,让客户�?1-5 星打分并给出建议�?0-80字�?;
+                "请生成一条自然友好的调研消息,让客户用1-5星打分并给出建议(20-80字)。";
             String surveyMsg = multiModelRouter != null
                 ? multiModelRouter.generateResponse(prompt, null, "survey_generator")
-                : "请问您对我们的服务满意吗?可以给个评价吗�?;
+                : "请问您对我们的服务满意吗?可以给个评价吗?";
             outputs.put("surveyType", surveyType);
             NodeExecutionResult r = NodeExecutionResult.success(outputs);
             r.setMessageToSend(surveyMsg.trim());
             return r;
         } catch (Exception e) {
             NodeExecutionResult r = NodeExecutionResult.success();
-            r.setMessageToSend("请问您对我们的服务满意吗�?);
+            r.setMessageToSend("请问您对我们的服务满意吗?");
             return r;
         }
     }
 
-    // ── 节点 12:用户画像(�?更新 lobster_user_profile�?──
+    // ── 节点 12:用户画像(AI 更新 lobster_user_profile)──
     private NodeExecutionResult handleUserProfileNode(int nodeType, String nodeConfig, ExecutionContext context) {
         try {
             JSONObject config = parseConfig(nodeConfig);
@@ -664,7 +666,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             if (auxMapper != null && extractFields != null && "infer".equals(profileType)) {
                 // AI 推断画像
                 String source = (context.getVariables() != null ? JSON.toJSONString(context.getVariables()) : "") +
-                    " 最新消�? " + (context.getLastMessage() != null ? context.getLastMessage() : "");
+                    " 最新消息: " + (context.getLastMessage() != null ? context.getLastMessage() : "");
                 try {
                     String aiPrompt = "推断客户画像字段: " + extractFields + "\n对话: " + source +
                         "\n输出JSON: {\"profile\":{\"field\":\"value\"}}";
@@ -675,7 +677,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                         for (String k : prof.keySet()) {
                             String v = prof.getString(k);
                             outputs.put(k, v);
-                            // 更新画像�?
+                            // 更新画像
                             auxMapper.update(String.format(
                                 "INSERT INTO lobster_user_profile(external_user_id, company_id, profile_key, profile_value, create_time) " +
                                 "VALUES('%s', %d, '%s', '%s', NOW()) ON DUPLICATE KEY UPDATE profile_value=VALUES(profile_value), update_time=NOW()",
@@ -688,8 +690,8 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             // 回读完整画像
             if (auxMapper != null) {
                 List<Map<String, Object>> rows = auxMapper.queryForList(
-                    "SELECT profile_key, profile_value FROM lobster_user_profile WHERE external_user_id=? AND company_id=?",
-                    context.getCustomerId(), context.getCompanyId());
+                    "SELECT profile_key, profile_value FROM lobster_user_profile WHERE external_user_id='" + sqlEscape(context.getCustomerId()) + "' AND company_id=" + context.getCompanyId(),
+                    context.getCompanyId());
                 for (Map<String, Object> r : rows) {
                     outputs.put((String) r.get("profile_key"), r.get("profile_value"));
                 }
@@ -701,7 +703,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         }
     }
 
-    // ── 节点 13:复�?回捞(读历史购买+AI生成复购话术�?──
+    // ── 节点 13:复购回捞(读历史购买+AI生成复购话术)──
     private NodeExecutionResult handleRepurchaseNode(int nodeType, String nodeConfig, ExecutionContext context) {
         try {
             JSONObject config = parseConfig(nodeConfig);
@@ -712,8 +714,8 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             if (auxMapper != null) {
                 try {
                     List<Map<String, Object>> orders = auxMapper.queryForList(
-                        "SELECT product_name, order_time FROM customer_order WHERE customer_id=? AND company_id=? ORDER BY order_time DESC LIMIT 3",
-                        context.getCustomerId(), context.getCompanyId());
+                        "SELECT product_name, order_time FROM customer_order WHERE customer_id='" + sqlEscape(context.getCustomerId()) + "' AND company_id=" + context.getCompanyId() + " ORDER BY order_time DESC LIMIT 3",
+                        context.getCompanyId());
                     if (!orders.isEmpty()) {
                         lastOrder = (String) orders.get(0).get("product_name");
                         outputs.put("lastOrderProduct", lastOrder);
@@ -725,7 +727,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                 "产品类别: " + productCategory + "\n" +
                 "上次购买: " + (lastOrder != null ? lastOrder : "未知") + "\n" +
                 "客户信息: " + (context.getVariables() != null ? JSON.toJSONString(context.getVariables()) : "{}") + "\n" +
-                "请生成一条自然的回捞/复购消息�?0-60字�?;
+                "请生成一条自然的回捞/复购消息(20-60字)。";
             String msg = multiModelRouter != null
                 ? multiModelRouter.generateResponse(prompt, null, "repurchase_generator")
                 : "亲~好久不见!最近有需要随时找我哈~";
@@ -777,22 +779,22 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
                     }
                 } catch (Exception e) { logger.warn("smart_api call failed: {}", e.getMessage()); }
             }
-            // 降级�?AI
+            // 降级AI
             return executeWithAI(nodeType, nodeConfig, context, null);
         } catch (Exception e) {
             return NodeExecutionResult.fail("智能API节点处理失败: " + e.getMessage());
         }
     }
 
-    // ── 节点 20:意图识别(真实语义分析�?──
+    // ── 节点 20:意图识别(真实语义分析──
     private NodeExecutionResult handleIntentRecognitionNode(int nodeType, String nodeConfig, ExecutionContext context) {
         try {
             JSONObject config = parseConfig(nodeConfig);
             String intentCategories = config != null ? config.getString("intentCategories") : "通用";
             Map<String, Object> outputs = new HashMap<>();
             if (context.getLastMessage() != null && !context.getLastMessage().isEmpty()) {
-                String prompt = "识别用户意图,类�? " + intentCategories + "\n消息: " + context.getLastMessage() +
-                    "\n输出JSON: {\"intent\":\"类别\",\"confidence\":0.8,\"keywords\":[\"�?\"]}";
+                String prompt = "识别用户意图,类别: " + intentCategories + "\n消息: " + context.getLastMessage() +
+                    "\n输出JSON: {\"intent\":\"类别\",\"confidence\":0.8,\"keywords\":[\"\"]}";
                 String aiResp = multiModelRouter.generateResponse(prompt, null, "intent_recognizer");
                 try {
                     JSONObject aiJson = JSON.parseObject(aiResp);
@@ -821,8 +823,8 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             Map<String, Object> outputs = new HashMap<>();
             if (context.getLastMessage() != null) {
                 String msg = context.getLastMessage().toLowerCase();
-                boolean needTakeover = msg.contains("人工") || msg.contains("转人�?) ||
-                    msg.contains("客服") || msg.contains("投诉") || msg.contains("退�?);
+                boolean needTakeover = msg.contains("人工") || msg.contains("转人工") ||
+                    msg.contains("客服") || msg.contains("投诉") || msg.contains("退款");
                 if (!needTakeover && multiModelRouter != null) {
                     String prompt = "判断是否需要转人工客服: " + context.getLastMessage() + "\n输出JSON: {\"needTakeover\":true/false,\"reason\":\"原因\"}";
                     try {
@@ -837,11 +839,11 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             }
             return NodeExecutionResult.success(outputs);
         } catch (Exception e) {
-            return NodeExecutionResult.fail("转人工检测失�? " + e.getMessage());
+            return NodeExecutionResult.fail("转人工检测失败: " + e.getMessage());
         }
     }
 
-    // ── 节点 22:质检评分(真实调 QualityScoringService�?──
+    // ── 节点 22:质检评分(真实调 QualityScoringService──
     private NodeExecutionResult handleQualityCheckNode(int nodeType, String nodeConfig, ExecutionContext context) {
         try {
             JSONObject config = parseConfig(nodeConfig);
@@ -870,7 +872,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
         }
     }
 
-    /** 安全解析 config,兼�?null 和空字符�?*/
+    /** 安全解析 config,兼容 null 和空字符串 */
     private JSONObject parseConfig(String cfg) {
         if (cfg == null || cfg.isEmpty() || "{}".equals(cfg)) return new JSONObject();
         try { return JSON.parseObject(cfg); } catch (Exception e) { return new JSONObject(); }
@@ -891,7 +893,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
             outputs.put(varName, varValue);
             return NodeExecutionResult.success(outputs);
         } catch (Exception e) {
-            return NodeExecutionResult.fail("变量赋值节点处理失�? " + e.getMessage());
+            return NodeExecutionResult.fail("变量赋值节点处理失败: " + e.getMessage());
         }
     }
 
@@ -921,7 +923,7 @@ public class DynamicNodeExecutorImpl implements DynamicNodeExecutor {
 
     private NodeExecutionResult handleLogisticsNotifyNode(int nodeType, String nodeConfig, ExecutionContext context) {
         NodeExecutionResult result = NodeExecutionResult.success();
-        result.setMessageToSend("您的订单已发货,物流单号�?234567890");
+        result.setMessageToSend("您的订单已发货,物流单号:1234567890");
         return result;
     }
 

+ 112 - 20
fs-service/src/main/java/com/fs/company/service/workflow/impl/DynamicNodeImplServiceImpl.java

@@ -1,12 +1,16 @@
 package com.fs.company.service.workflow.impl;
 
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.fs.company.service.workflow.DynamicNodeImplService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 import java.util.*;
+import java.util.stream.Collectors;
 
 @Service
 public class DynamicNodeImplServiceImpl implements com.fs.company.service.workflow.DynamicNodeImplService {
@@ -17,42 +21,130 @@ public class DynamicNodeImplServiceImpl implements com.fs.company.service.workfl
     private LobsterAuxiliaryMapper auxMapper;
 
     @Override
-    public List<Map<String, Object>> list(Long companyId, Integer nodeType) {
-        if (auxMapper == null) return new ArrayList<>();
-        return auxMapper.selectDynamicImpls(companyId, nodeType);
+    public String computeFingerprint(int nodeType, String nodeConfig) {
+        try {
+            String key = nodeType + ":" + (nodeConfig != null ? nodeConfig : "");
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            byte[] hash = md.digest(key.getBytes(StandardCharsets.UTF_8));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : hash) sb.append(String.format("%02x", b));
+            return sb.toString().substring(0, 16);
+        } catch (Exception e) {
+            return String.valueOf(Objects.hash(nodeType, nodeConfig));
+        }
     }
 
     @Override
-    public Long save(Long companyId, Integer nodeType, String implName, String implCode, String scriptContent, String status) {
+    public DynamicNodeImpl findActiveImpl(int nodeType, String fingerprint, Long companyId) {
         if (auxMapper == null) return null;
-        auxMapper.insertDynamicImpl(companyId, nodeType, implName, implCode, scriptContent, status != null ? status : "PENDING");
-        return auxMapper.selectLastInsertId();
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDynamicImpls(companyId, nodeType);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    String fp = (String) row.get("fingerprint");
+                    String status = (String) row.get("status");
+                    if (fingerprint != null && fingerprint.equals(fp) && "ACTIVE".equals(status)) {
+                        return mapToDto(row);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DynamicNodeImpl] findActiveImpl failed: {}", e.getMessage());
+        }
+        return null;
     }
 
     @Override
-    public void updateStatus(Long implId, String status) { if (auxMapper != null) auxMapper.updateStatus(implId, status); }
-
-    @Override
-    public void update(Long implId, String scriptContent, String status) { if (auxMapper != null) auxMapper.updateDynamicImpl(implId, scriptContent, status); }
+    public Long saveLearned(int nodeType, String fingerprint, String subDslJson,
+                            String promptUsed, String sourceModel, Long companyId) {
+        if (auxMapper == null) return null;
+        try {
+            auxMapper.insertDynamicImpl(companyId, nodeType, fingerprint, "auto_" + nodeType,
+                    subDslJson, "DRAFT");
+            return auxMapper.selectLastInsertId();
+        } catch (Exception e) {
+            logger.error("[DynamicNodeImpl] saveLearned failed: {}", e.getMessage());
+            return null;
+        }
+    }
 
     @Override
-    public void delete(Long implId) { if (auxMapper != null) auxMapper.deleteDynamicImpl(implId); }
+    public void recordRunAndScore(Long implId, int nodeType, Long companyId, Long instanceId,
+                                   String fingerprint, int durationMs, boolean success,
+                                   double score, String execPath, String inputCtx,
+                                   String outputResult, String errorMsg) {
+        if (auxMapper == null || implId == null) return;
+        try {
+            if (success && score >= 80) {
+                auxMapper.updateStatus(implId, "ACTIVE");
+            } else if (success && score >= 60) {
+                auxMapper.updateStatus(implId, "PENDING");
+            } else {
+                auxMapper.updateStatus(implId, "DRAFT");
+            }
+        } catch (Exception e) {
+            logger.warn("[DynamicNodeImpl] recordRunAndScore failed: {}", e.getMessage());
+        }
+    }
 
     @Override
-    public void disable(Long implId) { if (auxMapper != null) auxMapper.disableDynamicImpl(implId); }
+    public void approve(Long implId, String reviewer) {
+        if (auxMapper != null) {
+            try {
+                auxMapper.updateStatus(implId, "ACTIVE");
+            } catch (Exception e) {
+                logger.warn("[DynamicNodeImpl] approve failed: {}", e.getMessage());
+            }
+        }
+    }
 
     @Override
-    public Double getAvgQualityScore(Integer nodeType, Long companyId) {
-        if (auxMapper == null) return null;
-        return auxMapper.selectAvgQualityScore(nodeType, companyId);
+    public void reject(Long implId, String reviewer, String reason) {
+        if (auxMapper != null) {
+            try {
+                auxMapper.updateStatus(implId, "REJECTED");
+            } catch (Exception e) {
+                logger.warn("[DynamicNodeImpl] reject failed: {}", e.getMessage());
+            }
+        }
     }
 
     @Override
-    public String getStatus(Long implId) { if (auxMapper == null) return "UNKNOWN"; return auxMapper.selectStatus(implId); }
+    public List<DynamicNodeImpl> listByStatus(String status, Long companyId) {
+        List<DynamicNodeImpl> result = new ArrayList<>();
+        if (auxMapper == null) return result;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDynamicImpls(companyId, null);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    String s = (String) row.get("status");
+                    if (status == null || status.equals(s)) {
+                        result.add(mapToDto(row));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DynamicNodeImpl] listByStatus failed: {}", e.getMessage());
+        }
+        return result;
+    }
 
-    @Override
-    public List<Map<String, Object>> listPaged(Long companyId, int page, int pageSize) {
-        if (auxMapper == null) return new ArrayList<>();
-        return auxMapper.selectDynamicImplPaged(companyId, (page - 1) * pageSize, pageSize);
+    private DynamicNodeImpl mapToDto(Map<String, Object> row) {
+        DynamicNodeImpl dto = new DynamicNodeImpl();
+        dto.setId(row.get("id") instanceof Number ? ((Number) row.get("id")).longValue() : null);
+        dto.setCompanyId(row.get("company_id") instanceof Number ? ((Number) row.get("company_id")).longValue() : null);
+        dto.setNodeType(row.get("node_type") instanceof Number ? ((Number) row.get("node_type")).intValue() : null);
+        dto.setNodeTypeCode((String) row.get("node_type_code"));
+        dto.setFingerprint((String) row.get("fingerprint"));
+        dto.setSubDslJson((String) row.get("script_content"));
+        dto.setPromptUsed((String) row.get("prompt_used"));
+        dto.setSourceModel((String) row.get("source_model"));
+        dto.setQualityScore(row.get("quality_score") instanceof Number ? ((Number) row.get("quality_score")).doubleValue() : null);
+        dto.setExecCount(row.get("exec_count") instanceof Number ? ((Number) row.get("exec_count")).intValue() : 0);
+        dto.setSuccessCount(row.get("success_count") instanceof Number ? ((Number) row.get("success_count")).intValue() : 0);
+        dto.setAvgDurationMs(row.get("avg_duration_ms") instanceof Number ? ((Number) row.get("avg_duration_ms")).intValue() : 0);
+        dto.setStatus((String) row.get("status"));
+        dto.setReviewedBy((String) row.get("reviewed_by"));
+        return dto;
     }
 }

+ 3 - 0
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterEvolutionEngineImpl.java

@@ -1,5 +1,6 @@
 package com.fs.company.service.workflow.impl;
 
+import com.alibaba.fastjson.JSON;
 import com.fs.company.service.llm.MultiModelRouter;
 import com.fs.company.service.workflow.*;
 import com.fs.company.service.workflow.identity.IdentityHidingService;
@@ -8,6 +9,8 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.*;
+
 import java.util.HashMap;
 import java.util.Map;
 

+ 15 - 1
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterLearningCorpusServiceImpl.java

@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
+import java.util.Map;
 
 @Service
 public class LobsterLearningCorpusServiceImpl implements ILobsterLearningCorpusService {
@@ -16,6 +17,19 @@ public class LobsterLearningCorpusServiceImpl implements ILobsterLearningCorpusS
 
     @Override
     public List<LobsterLearningCorpus> selectListByCompanyId(Long companyId, String scenario, String status) {
-        return corpusMapper.selectListByCompanyId(companyId, scenario, status);
+        List<Map<String, Object>> rows = corpusMapper.selectByScenario(companyId, scenario, 100);
+        List<LobsterLearningCorpus> result = new java.util.ArrayList<>();
+        if (rows != null) {
+            for (Map<String, Object> row : rows) {
+                LobsterLearningCorpus c = new LobsterLearningCorpus();
+                c.setId(((Number) row.get("id")).longValue());
+                c.setCompanyId(companyId);
+                c.setCustomerQuestion((String) row.get("customer_question"));
+                c.setSalesAnswer((String) row.get("sales_answer"));
+                c.setScenario((String) row.get("scenario"));
+                result.add(c);
+            }
+        }
+        return result;
     }
 }

+ 32 - 5
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterSalesCorpusServiceImpl.java

@@ -19,8 +19,26 @@ public class LobsterSalesCorpusServiceImpl implements ILobsterSalesCorpusService
     @Override
     public Map<String, Object> listCorpus(int page, int size, Long companyId, String scenario, String status) {
         int offset = (page - 1) * size;
-        List<LobsterSalesCorpus> list = corpusMapper.selectList(companyId, scenario, status, offset, size);
-        long total = corpusMapper.countList(companyId, scenario, status);
+        List<Map<String, Object>> allRows = corpusMapper.selectAll(companyId);
+        List<LobsterSalesCorpus> filtered = new java.util.ArrayList<>();
+        if (allRows != null) {
+            for (Map<String, Object> row : allRows) {
+                String rScenario = (String) row.get("scenario");
+                if (scenario != null && !scenario.isEmpty() && !scenario.equals(rScenario)) continue;
+                LobsterSalesCorpus c = new LobsterSalesCorpus();
+                c.setId(((Number) row.get("id")).longValue());
+                c.setCompanyId(companyId);
+                c.setScenario(rScenario);
+                c.setSalesAnswer((String) row.get("content"));
+                c.setSourceType((String) row.get("source"));
+                c.setScore(row.get("score") != null ? java.math.BigDecimal.valueOf(((Number) row.get("score")).doubleValue()) : java.math.BigDecimal.ZERO);
+                filtered.add(c);
+            }
+        }
+        long total = filtered.size();
+        int from = Math.min(offset, filtered.size());
+        int to = Math.min(offset + size, filtered.size());
+        List<LobsterSalesCorpus> list = filtered.subList(from, to);
 
         Map<String, Object> result = new HashMap<>();
         result.put("list", list);
@@ -32,12 +50,21 @@ public class LobsterSalesCorpusServiceImpl implements ILobsterSalesCorpusService
 
     @Override
     public void addCorpus(LobsterSalesCorpus corpus, String username) {
-        corpus.setCreateBy(username);
-        corpusMapper.insert(corpus);
+        corpusMapper.insert(corpus.getCompanyId(), corpus.getScenario(),
+                corpus.getSalesAnswer(), corpus.getSourceType(),
+                corpus.getScore() != null ? corpus.getScore().doubleValue() : 0.0);
     }
 
     @Override
     public List<String> getScenarios() {
-        return corpusMapper.selectScenarios();
+        List<Map<String, Object>> rows = corpusMapper.selectAll(null);
+        java.util.Set<String> set = new java.util.LinkedHashSet<>();
+        if (rows != null) {
+            for (Map<String, Object> row : rows) {
+                String s = (String) row.get("scenario");
+                if (s != null) set.add(s);
+            }
+        }
+        return new java.util.ArrayList<>(set);
     }
 }

+ 123 - 123
fs-service/src/main/java/com/fs/company/service/workflow/impl/LobsterWorkflowExecutorImpl.java

@@ -13,6 +13,7 @@ import com.fs.company.mapper.CompanyWorkflowLobsterMapper;
 import com.fs.company.mapper.CompanyWorkflowLobsterNodeMapper;
 import com.fs.company.mapper.LobsterChatSessionMapper;
 import com.fs.company.mapper.LobsterChatMsgMapper;
+import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import com.fs.company.mapper.LobsterNodeExecutionLogMapper;
 import com.fs.company.mapper.LobsterWorkflowInstanceMapper;
 import com.fs.company.domain.LobsterChatSession;
@@ -95,11 +96,11 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     @Autowired
     private SemanticAnalyzer semanticAnalyzer;
 
-    /** 个性化引擎:实现千人千面,根据用户画像和偏好定制消息内�?*/
+    /** 个性化引擎:实现千人千面,根据用户画像和偏好定制消息内*/
     @Autowired
     private PersonalizationEngine personalizationEngine;
 
-    /** 用户级节点优化器:针对特定用户自动优化后续节点内�?*/
+    /** 用户级节点优化器:针对特定用户自动优化后续节点内*/
     @Autowired
     private com.fs.company.service.workflow.evolution.UserNodeOptimizer userNodeOptimizer;
 
@@ -119,7 +120,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     private DuplicateReplyDetector dedupDetector;
 
     @Autowired(required = false)
-    private auxMapper auxMapper;
+    private LobsterAuxiliaryMapper auxMapper;
 
     @Autowired(required = false)
     private ChannelTypeRegistry channelRegistry;
@@ -152,7 +153,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
         List<CompanyWorkflowLobsterNode> nodes = nodeMapper.selectByWorkflowIdAndCompanyId(workflowId, companyId);
         if (nodes == null || nodes.isEmpty()) {
-            return AjaxResult.error("工作流节点为�?);
+            return AjaxResult.error("工作流节点为空");
         }
 
         nodes.sort(Comparator.comparingInt(CompanyWorkflowLobsterNode::getSortNo));
@@ -199,41 +200,41 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
         MessageChannelResult sendResult = deliverMessage(companyId, contactId, channelType, message, enrichedVars, instance.getId(), workflowId);
         if (sendResult != null && !sendResult.isSuccess()) {
-            logger.warn("首节点消息发送失�? instanceId={}, error={}", instance.getId(), sendResult.getErrorMsg());
+            logger.warn("首节点消息发送失败, instanceId={}, error={}", instance.getId(), sendResult.getErrorMsg());
         }
 
         heartbeatScheduler.registerInstance(companyId, instance.getId(),
                 HeartbeatConfig.defaultConfig(companyId, instance.getId(), workflowId, contactId, channelType));
 
-        return AjaxResult.success("工作流启动成�?, instance);
+        return AjaxResult.success("工作流启动成功", instance);
     }
 
     /**
-     * 节点类型常量(与前端 visual.vue 1-12 + skill.md 13/14 对齐�?
-     * 编号统一规则�?
-     *   1  开�?   2  消息(AI回复)   3  判断   4  等待   5  结束
-     *   6  API     7  购物�?成单)   8  优惠�?订单)   9  标签
+     * 节点类型常量(与前端 visual.vue 1-12 + skill.md 13/14 对齐
+     * 编号统一规则
+     *   1  开   2  消息(AI回复)   3  判断   4  等待   5  结束
+     *   6  API     7  购物车(成单)   8  优惠券(订单)   9  标签
      *   10 赠礼(关怀)  11 文档(调查)   12 用户(画像)
      *   13 复购(skill.md 新增)   14 智能API(skill.md 新增)
      * 15/16/99 为历史兼容编号,保留不删,新数据不要再用
      */
     private static final int NODE_TYPE_START = 1;
-    private static final int NODE_TYPE_AI_PROCESS = 2;       // 消息节点(AI 回复�?
-    private static final int NODE_TYPE_SEND_MESSAGE = 3;     // 判断节点(前�?visual.vue 编号 3�?
+    private static final int NODE_TYPE_AI_PROCESS = 2;       // 消息节点(AI 回复
+    private static final int NODE_TYPE_SEND_MESSAGE = 3;     // 判断节点(前端 visual.vue 编号 3)
     private static final int NODE_TYPE_WAIT = 4;
-    private static final int NODE_TYPE_CONDITION = 5;        // 结束节点(前�?visual.vue 编号 5)—�?历史名保�?
+    private static final int NODE_TYPE_CONDITION = 5;        // 结束节点(前端 visual.vue 编号 5)——历史名保留
     private static final int NODE_TYPE_TASK = 6;             // API 节点
-    private static final int NODE_TYPE_COLLECT_INFO = 7;     // 购物车节点(≈skill.md 成单�?
-    private static final int NODE_TYPE_TRANSFER_HUMAN = 8;   // 优惠券节点(≈skill.md 订单)—�?历史名保�?
+    private static final int NODE_TYPE_COLLECT_INFO = 7;     // 购物车节点(≈skill.md 成单
+    private static final int NODE_TYPE_TRANSFER_HUMAN = 8;   // 优惠券节点(≈skill.md 订单)——历史名保留
     private static final int NODE_TYPE_TAG_OPERATION = 9;    // 标签节点
-    private static final int NODE_TYPE_HTTP_CALL = 10;       // 赠礼节点(≈skill.md 关怀�?
-    private static final int NODE_TYPE_RAG_QUERY = 11;       // 文档节点(≈skill.md 调查�?
-    private static final int NODE_TYPE_CODE_EXEC = 12;       // 用户节点(≈skill.md 画像�?
-    private static final int NODE_TYPE_LOOP = 13;            // 复购节点(skill.md 新增�?
-    private static final int NODE_TYPE_DB_QUERY = 14;        // 智能 API 节点(skill.md 新增�?
+    private static final int NODE_TYPE_HTTP_CALL = 10;       // 赠礼节点(≈skill.md 关怀
+    private static final int NODE_TYPE_RAG_QUERY = 11;       // 文档节点(≈skill.md 调查
+    private static final int NODE_TYPE_CODE_EXEC = 12;       // 用户节点(≈skill.md 画像
+    private static final int NODE_TYPE_LOOP = 13;            // 复购节点(skill.md 新增
+    private static final int NODE_TYPE_DB_QUERY = 14;        // 智能 API 节点(skill.md 新增
     private static final int NODE_TYPE_SUB_WORKFLOW = 15;    // 历史保留
     private static final int NODE_TYPE_VARIABLE = 16;        // 历史保留
-    private static final int NODE_TYPE_END = 99;             // 历史保留(新数据�?5�?
+    private static final int NODE_TYPE_END = 99;             // 历史保留(新数据用 5)
 
     @Autowired(required = false)
     private MultiTurnDialogueManager multiTurnDialogueManager;
@@ -249,7 +250,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             return AjaxResult.error("工作流实例不存在");
         }
         if (!"running".equals(instance.getStatus())) {
-            return AjaxResult.error("工作流实例不在运行状�?);
+            return AjaxResult.error("工作流实例不在运行状态");
         }
         /* 人工接管模式: 跳过AI处理, 等待人工回复 */
         if ("human".equals(instance.getControlMode())) {
@@ -263,7 +264,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
         List<CompanyWorkflowLobsterNode> nodes = nodeMapper.selectByWorkflowIdAndCompanyId(instance.getWorkflowId(), companyId);
         if (nodes == null || nodes.isEmpty()) {
-            return AjaxResult.error("工作流节点为�?);
+            return AjaxResult.error("工作流节点为空");
         }
         nodes.sort(Comparator.comparingInt(CompanyWorkflowLobsterNode::getSortNo));
 
@@ -278,13 +279,13 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         String nodeCode = currentNode.getNodeCode();
 
         /*
-         * ===== 处理客户回复:语义分�?+ 节点类型路由 =====
+         * ===== 处理客户回复:语义分+ 节点类型路由 =====
          */
         if (customerReply != null && !customerReply.isEmpty()) {
             /* 1. 记录已接收的日志 */
             logNodeExecution(companyId, instanceId, instance.getWorkflowId(), currentIndex, currentNode, null, customerReply, "received");
 
-            /* 1.5 客户消息速率风控�?秒内超过5条消�?�?触发冷却/转人�?*/
+            /* 1.5 客户消息速率风控:10秒内超过5条消息,触发冷却/转人工 */
             String rateKey = "rate_" + instance.getContactId() + "_" + instanceId;
             long now = System.currentTimeMillis();
             Object rtObj = variables.get(rateKey);
@@ -293,22 +294,22 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             if (now - rateWindow[1] > 5_000) { newCount = 1; }
             variables.put(rateKey, new long[]{newCount, now});
             if (newCount > 5) {
-                logger.warn("[LobsterWorkflow] 客户消息速率异常: instanceId={}, {}�?5�? 冷却30�?, instanceId, newCount);
+                logger.warn("[LobsterWorkflow] 客户消息速率异常: instanceId={}, count={}", instanceId, newCount);
                 String cooldownReply = "您发的消息有点快,请稍等片刻,我马上回来~";
                 deliverMessage(companyId, instance.getContactId(), channelType, cooldownReply, variables, instanceId, instance.getWorkflowId());
                 variables.put("_cooldown_until", System.currentTimeMillis() + 30_000);
                 instance.setVariables(JSON.toJSONString(variables));
                 instance.setUpdateTime(DateUtils.getNowDate());
                 instanceMapper.updateById(instance);
-                return AjaxResult.error("消息频率过高,已进入30秒冷�?);
+                return AjaxResult.error("消息频率过高,已进入30秒冷却");
             }
 
-            // 冷却期检�?
+            // 冷却期检
             Object cdObj = variables.get("_cooldown_until");
             if (cdObj instanceof Number) {
                 long cooldownUntil = ((Number) cdObj).longValue();
                 if (System.currentTimeMillis() < cooldownUntil) {
-                    return AjaxResult.error("冷却中,请稍后再�?);
+                    return AjaxResult.error("冷却中,请稍后再试");
                 }
             }
 
@@ -318,12 +319,12 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             if (gcObj instanceof Number) globalCount = ((Number) gcObj).intValue();
             variables.put("replyCount", globalCount + 1);
 
-            /* 2.5 死循环防护:全局上限 + 节点访问频率检�?*/
+            /* 2.5 死循环防护:全局上限 + 节点访问频率检*/
             if (globalCount + 1 > 50) {
                 logger.warn("[LobsterWorkflow] 全局回复超过50轮,强制终止: instanceId={}", instanceId);
                 completeInstance(instance);
                 heartbeatScheduler.unregisterInstance(instanceId);
-                return AjaxResult.error("工作流交互轮次过多,已自动终�?);
+                return AjaxResult.error("工作流交互轮次过多,已自动终止");
             }
             String prevNode = (String) variables.get("_prevNodeCode");
             String transitionKey = prevNode + "->" + nodeCode;
@@ -333,14 +334,14 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             transitionCount++;
             variables.put("_trans_" + transitionKey, transitionCount);
             if (transitionCount > 8) {
-                logger.warn("[LobsterWorkflow] 节点跳转死循环检�? {} 出现{}次,强制终止", transitionKey, transitionCount);
+                logger.warn("[LobsterWorkflow] 节点跳转死循环检测,{} 出现{}次,强制终止", transitionKey, transitionCount);
                 completeInstance(instance);
                 heartbeatScheduler.unregisterInstance(instanceId);
                 return AjaxResult.error("检测到死循环,工作流已自动终止");
             }
             variables.put("_prevNodeCode", nodeCode);
 
-            /* 3. 单节点轮次计�?*/
+            /* 3. 单节点轮次计*/
             String nodeRoundKey = "nodeRound_" + nodeCode;
             int nodeRound = 0;
             Object nrObj = variables.get(nodeRoundKey);
@@ -357,11 +358,11 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
             recordEvolution(companyId, instance, variables, customerReply, semanticResult);
 
-            /* 语义分析AI计费:按Token量估算(中文�?.5 token/字) */
+            /* 语义分析AI计费:按Token量估算(中文约1.5 token/字) */
             int estimatedTokens = customerReply != null ? (int)(customerReply.length() * 1.5) + 200 : 200;
             billingService.tryConsumeByTokens(companyId, estimatedTokens, 100, defaultAiModel);
 
-            /* 5. COLLECT_INFO 节点:收集信�?*/
+            /* 5. COLLECT_INFO 节点:收集信*/
             if (nodeType != null && nodeType == NODE_TYPE_COLLECT_INFO) {
                 return handleCollectInfo(companyId, instance, currentNode, nodes, currentIndex, variables, channelType, customerReply, nodeRound);
             }
@@ -371,7 +372,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 return handleTransferHuman(companyId, instance, currentNode, variables, customerReply);
             }
 
-            /* 7. 多轮对话检查:max_rounds > 0 且未达上�?�?停留当前节点 */
+            /* 7. 多轮对话检查:max_rounds > 0 且未达上限,停留当前节点 */
             Integer maxRounds = currentNode.getMaxRounds();
             if (maxRounds != null && maxRounds > 0 && nodeRound < maxRounds) {
                 String repeatMessage = generateNodeMessage(currentNode, variables);
@@ -393,10 +394,10 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 result.put("nodeRound", nodeRound);
                 result.put("maxRounds", maxRounds);
                 result.put("stayOnNode", true);
-                return AjaxResult.success("多轮对话�? + nodeRound + "/" + maxRounds + "�?, result);
+                return AjaxResult.success("多轮对话(" + nodeRound + "/" + maxRounds + ")", result);
             }
 
-            /* 8. MultiTurnDialogueManager 集成:专业多轮对话管�?*/
+            /* 8. MultiTurnDialogueManager 集成:专业多轮对话管*/
             if (multiTurnDialogueManager != null && nodeType != null &&
                 (nodeType == NODE_TYPE_SEND_MESSAGE || nodeType == NODE_TYPE_AI_PROCESS)) {
                 MultiTurnDialogueManager.DialogueResult dialogueResult = multiTurnDialogueManager.processDialogue(
@@ -416,12 +417,12 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                     result.put("message", dialogueResult.getReply());
                     result.put("stayOnNode", true);
                     result.put("dialogueRound", dialogueResult.getCurrentRound());
-                    return AjaxResult.success("多轮对话(MM)�? + dialogueResult.getCurrentRound() + "�?, result);
+                    return AjaxResult.success("多轮对话(MM)(" + dialogueResult.getCurrentRound() + ")", result);
                 }
                 variables.putAll(dialogueResult.getCollectedVariables());
             }
         } else {
-            /* 无客户回复:推进前先记录前序节点发�?*/
+            /* 无客户回复:推进前先记录前序节点发*/
             logNodeExecution(companyId, instanceId, instance.getWorkflowId(), currentIndex, currentNode, null, null, "received");
         }
 
@@ -484,7 +485,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             return handleTagOperation(companyId, instance, nextNode, nodes, currentIndex, nextIndex, variables, channelType);
         }
 
-        /* 未知节点类型 �?AI动态生成执行逻辑 */
+        /* 未知节点类型 AI动态生成执行逻辑 */
         if (nextNode.getNodeType() != null && nextNode.getNodeType() > 0 &&
             nextNode.getNodeType() != NODE_TYPE_START && nextNode.getNodeType() != NODE_TYPE_END) {
             boolean isKnown = false;
@@ -511,7 +512,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
         MessageChannelResult sendResult = deliverMessage(companyId, instance.getContactId(), channelType, message, variables, instanceId, instance.getWorkflowId());
         if (sendResult != null && !sendResult.isSuccess()) {
-            logger.warn("节点消息发送失�? instanceId={}, nodeIndex={}, error={}", instanceId, nextIndex, sendResult.getErrorMsg());
+            logger.warn("节点消息发送失败, instanceId={}, nodeIndex={}, error={}", instanceId, nextIndex, sendResult.getErrorMsg());
         }
 
         Map<String, Object> result = new HashMap<>();
@@ -525,7 +526,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * COLLECT_INFO 节点:多轮信息收�?
+     * COLLECT_INFO 节点:多轮信息收
      * 从nodeConfig中读取collect_fields配置,逐轮询问每个字段
      * 全部收集完毕后推进到下一节点
      */
@@ -593,10 +594,10 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             result.put("collectField", nextField);
             result.put("collectProgress", (collectedIndex + 1) + "/" + fieldsToCollect.size());
             result.put("stayOnNode", true);
-            return AjaxResult.success("信息采集�?" + (collectedIndex + 1) + "/" + fieldsToCollect.size() + ")", result);
+            return AjaxResult.success("信息采集中(" + (collectedIndex + 1) + "/" + fieldsToCollect.size() + ")", result);
         }
 
-        /* 信息收集完毕 �?推进 */
+        /* 信息收集完毕 推进 */
         message = completionMessage;
         if (message.contains("${")) message = varEngine.substitute(message, variables);
 
@@ -649,7 +650,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
 
         String nodeConfig = node.getNodeConfig();
         String handoffMsg = promptService != null ?
-                promptService.getContent("handoff_default", null) : "已为您转接人工客服,请稍�?..";
+                promptService.getContent("handoff_default", null) : "已为您转接人工客服,请稍候...";
         if (nodeConfig != null && !nodeConfig.isEmpty()) {
             try {
                 JSONObject config = JSON.parseObject(nodeConfig);
@@ -681,30 +682,30 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult pauseWorkflow(Long companyId, Long instanceId) {
         LobsterWorkflowInstance instance = instanceMapper.selectByIdAndCompanyId(instanceId, companyId);
-        if (instance == null) return AjaxResult.error("实例不存�?);
+        if (instance == null) return AjaxResult.error("实例不存在");
         instanceMapper.updateStatus(instanceId, companyId, "paused");
         heartbeatScheduler.unregisterInstance(instanceId);
-        return AjaxResult.success("已暂�?);
+        return AjaxResult.success("已暂停");
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult resumeWorkflow(Long companyId, Long instanceId) {
         LobsterWorkflowInstance instance = instanceMapper.selectByIdAndCompanyId(instanceId, companyId);
-        if (instance == null) return AjaxResult.error("实例不存�?);
+        if (instance == null) return AjaxResult.error("实例不存在");
         instanceMapper.updateStatus(instanceId, companyId, "running");
         Map<String, Object> variables = parseVariables(instance.getVariables());
         String channelType = (String) variables.getOrDefault("channelType", "QW");
         heartbeatScheduler.registerInstance(companyId, instanceId,
                 HeartbeatConfig.defaultConfig(companyId, instanceId, instance.getWorkflowId(), instance.getContactId(), channelType));
-        return AjaxResult.success("已恢�?);
+        return AjaxResult.success("已恢复");
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult terminateWorkflow(Long companyId, Long instanceId, String reason) {
         LobsterWorkflowInstance instance = instanceMapper.selectByIdAndCompanyId(instanceId, companyId);
-        if (instance == null) return AjaxResult.error("实例不存�?);
+        if (instance == null) return AjaxResult.error("实例不存在");
         instance.setStatus("terminated");
         instance.setErrorMessage(reason);
         instance.setEndTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
@@ -712,7 +713,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         instance.setUpdateTime(DateUtils.getNowDate());
         instanceMapper.updateById(instance);
         heartbeatScheduler.unregisterInstance(instanceId);
-        return AjaxResult.success("已终�?);
+        return AjaxResult.success("已终止");
     }
 
     @Override
@@ -747,7 +748,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         try {
             return contactAdapterRouter.resolve(companyId, contactId, channelType);
         } catch (Exception e) {
-            logger.warn("解析联系人信息失�? contactId={}, channelType={}", contactId, channelType, e);
+            logger.warn("解析联系人信息失败, contactId={}, channelType={}", contactId, channelType, e);
             return null;
         }
     }
@@ -756,12 +757,12 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                                                  String message, Map<String, Object> variables,
                                                  Long instanceId, Long workflowId) {
         try {
-            /* 重复检测:跳过已发送过的相同消�?*/
+            /* 重复检测:跳过已发送过的相同消*/
             if (dedupDetector != null && message != null && !message.isEmpty()) {
                 if (dedupDetector.isDuplicate(contactId, message)) {
                     message = dedupDetector.rewriteIfDuplicate(contactId, message);
                     if (message == null) {
-                        logger.info("[Dedup] 消息已重复且无法改写, 跳过发�? contactId={}", contactId);
+                        logger.info("[Dedup] 消息已重复且无法改写, 跳过发送, contactId={}", contactId);
                         return MessageChannelResult.ok(channelType, "dedup_skipped");
                     }
                 }
@@ -798,12 +799,11 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             logger.error("消息触达失败: companyId={}, contactId={}, channelType={}", companyId, contactId, channelType, e);
             /* 记录到死信队列,自动重试 */
             if (deadLetterQueue != null && instanceId != null) {
-                deadLetterQueue.recordFailure(instanceId, companyId, contactId, channelType,
-                        message, e.getMessage());
+                deadLetterQueue.enqueue(companyId, "msg_delivery", message, e.getMessage());
             }
             return MessageChannelResult.fail(channelType, "消息触达异常: " + e.getMessage());
         } finally {
-            /* 渠道消息计费(每发送一条消息扣企微助手余额�?*/
+            /* 渠道消息计费(每发送一条消息扣企微助手余额*/
             if (channelType != null) {
                 billingService.tryConsume(companyId, BillingService.CONSUME_WECHAT_HELPER,
                         new java.math.BigDecimal("0.005"), "龙虾引擎消息发送[" + channelType + "]");
@@ -812,10 +812,10 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * 同步写入chat_msg�? 打通ChatSession聚合页面
+     * 同步写入chat_msg 打通ChatSession聚合页面
      * 以lobster_unified_contact为桥梁,支持任意渠道即插即用
      * 
-     * 架构: contact_id(channelType) �?lobster_unified_contact �?chat_session(contact_id+channel_source_id)
+     * 架构: contact_id(channelType) → lobster_unified_contact → chat_session(contact_id+channel_source_id)
      */
     private void bridgeToChatMsg(Long companyId, Long contactId, String channelType,
                                   String message, Long instanceId, boolean success) {
@@ -847,13 +847,13 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * 会话查找/创建 �?以lobster_unified_contact为统一桥梁
+     * 会话查找/创建 以lobster_unified_contact为统一桥梁
      * 
      * 支持渠道: QW(qw_user) / WX(company_wx_user) / IM(im_user) / 
      *          WHATSAPP(whatsapp_contact) / LINE(line_contact) / 
      *          TELEGRAM(telegram_contact) / APP_IM(app_im_user) / OTHER
      * 
-     * 新增渠道: 只需在ChannelTypeRegistry中注册即�? 无需修改此处代码
+     * 新增渠道: 只需在ChannelTypeRegistry中注册即可, 无需修改此处代码
      */
     private Long findOrCreateSession(Long companyId, Long contactId, String channelType, Long instanceId) {
         if (chatSessionMapper == null) return null;
@@ -909,27 +909,27 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             context.setVariables(variables);
             evolutionEngine.recordInteraction(context);
         } catch (Exception e) {
-            logger.warn("记录进化上下文失�? instanceId={}", instance.getId(), e);
+            logger.warn("记录进化上下文失败, instanceId={}", instance.getId(), e);
         }
     }
 
     /**
-     * 生成节点消息(千人千�?用户级优化版�?
+     * 生成节点消息(千人千面/用户级优化版本)
      * 
-     * 消息生成流程�?
-     * 1. 变量替换:将模板中的${xxx}替换为实际变量�?
-     * 2. 个性化定制:调用PersonalizationEngine根据用户画像和偏好定制消�?
-     *    - 分群话术覆盖:不同用户分群使用不同话�?
+     * 消息生成流程
+     * 1. 变量替换:将模板中的${xxx}替换为实际变量
+     * 2. 个性化定制:调用PersonalizationEngine根据用户画像和偏好定制消
+     *    - 分群话术覆盖:不同用户分群使用不同话
      *    - 用户变量替换:替换用户专属变量(昵称、问候语等)
-     *    - 语气调整:根据分群策略调整语气(正式/轻松/温暖�?
+     *    - 语气调整:根据分群策略调整语气(正式/轻松/温暖
      * 3. 用户级节点优化:查询该用户是否有已应用的优化内容
      *    - 如果龙虾引擎针对该用户优化了此节点,使用优化后的内容
      *    - 优化内容经过审核确认后才应用(根据配置决定是否需要人工确认)
-     * 4. 用户偏好学习:记录本次消息发送的偏好信息,用于后续优�?
+     * 4. 用户偏好学习:记录本次消息发送的偏好信息,用于后续优
      * 5. 记录交互数据:将交互数据传给UserNodeOptimizer用于后续分析
      *
      * @param node      工作流节点,包含消息模板
-     * @param variables 变量上下文,包含用户信息和业务变�?
+     * @param variables 变量上下文,包含用户信息和业务变
      * @return 个性化+优化后的消息内容
      */
     private String generateNodeMessage(CompanyWorkflowLobsterNode node, Map<String, Object> variables) {
@@ -960,7 +960,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                             companyId, userId, node.getNodeCode());
                 }
             } catch (Exception e) {
-                logger.warn("[LobsterWorkflow] 个性化定制失败,使用原始消�? companyId={}, nodeCode={}",
+                logger.warn("[LobsterWorkflow] 个性化定制失败,使用原始消息, companyId={}, nodeCode={}",
                         companyId, node.getNodeCode(), e);
             }
 
@@ -970,14 +970,14 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                         companyId, userId, node.getNodeCode());
                 if (optimizedContent != null && !optimizedContent.isEmpty()) {
                     message = optimizedContent;
-                    logger.info("[LobsterWorkflow] 使用用户级优化内�? companyId={}, userId={}, nodeCode={}",
+                    logger.info("[LobsterWorkflow] 使用用户级优化内容, companyId={}, userId={}, nodeCode={}",
                             companyId, userId, node.getNodeCode());
                 }
             } catch (Exception e) {
-                logger.debug("[LobsterWorkflow] 查询用户级优化内容失�? {}", e.getMessage());
+                logger.debug("[LobsterWorkflow] 查询用户级优化内容失败, {}", e.getMessage());
             }
 
-            /* 第四步:学习用户偏好(异步,不影响主流程�?*/
+            /* 第四步:学习用户偏好(异步,不影响主流程*/
             try {
                 if (variables.containsKey("customerIntent")) {
                     personalizationEngine.learnUserPreference(
@@ -993,7 +993,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 logger.debug("[LobsterWorkflow] 学习用户偏好失败: {}", e.getMessage());
             }
 
-            /* 第五步:记录交互数据到UserNodeOptimizer,用于后续优化分�?*/
+            /* 第五步:记录交互数据到UserNodeOptimizer,用于后续优化分*/
             try {
                 Map<String, Object> interaction = new HashMap<>();
                 interaction.put("sentMessage", message);
@@ -1151,8 +1151,8 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * RAG_QUERY 节点:知识库检�?
-     * 双路召回:向量语义检�?VectorPatternMatcher) + LLM兜底
+     * RAG_QUERY 节点:知识库检
+     * 双路召回:向量语义检索(VectorPatternMatcher) + LLM兜底
      * nodeConfig: {"knowledgeBase":"course_kb", "topK":3, "threshold":0.7}
      */
     private AjaxResult handleRagQuery(Long companyId, LobsterWorkflowInstance instance,
@@ -1183,7 +1183,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         String ragResult;
         StringBuilder sources = new StringBuilder();
 
-        /* 第一路:向量语义检索(VectorPatternMatcher�?*/
+        /* 第一路:向量语义检索(VectorPatternMatcher*/
         if (vectorPatternMatcher != null) {
             try {
                 List<VectorPatternMatcher.VectorMatchResult> vectorResults =
@@ -1192,7 +1192,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                     StringBuilder context = new StringBuilder();
                     for (int i = 0; i < vectorResults.size(); i++) {
                         VectorPatternMatcher.VectorMatchResult vr = vectorResults.get(i);
-                        context.append("【来�?).append(i + 1).append("�?);
+                        context.append("【来源").append(i + 1).append("】");
                         if (vr.getKey() != null) context.append(vr.getKey()).append(": ");
                         context.append(vr.getText()).append("\n");
                         if (sources.length() > 0) sources.append(",");
@@ -1205,20 +1205,20 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                     ragVars.put("context", context.toString());
                     String llmPrompt = promptService != null ?
                             promptService.getContent("rag_vector_llm", ragVars) :
-                            ("根据以下知识库内容回答问题,如果内容不相关请直接说明:\n\n【问题�? + query + "\n\n【知识库内容】\n" + context + "\n请用简洁的语言回答,引用来源时标注【来源N】�?);
-                    String llmSystemRole = promptService != null ? promptService.getSystemRole("rag_vector_llm") : "你是企业知识库助手,只基于提供的知识回答问题";
-                    String ragModel = promptService != null ? promptService.getModelName("rag_vector_llm") : defaultAiModel;
+                            ("根据以下知识库内容回答问题,如果内容不相关请直接说明:\n\n【问题】" + query + "\n\n【知识库内容】\n" + context + "\n请用简洁的语言回答,引用来源时标注【来源N】");
+                    String llmSystemRole = promptService != null ? promptService.getSystemRole("rag_vector_llm", companyId, null) : "你是企业知识库助手,只基于提供的知识回答问题";
+                    String ragModel = promptService != null ? promptService.getModelName("rag_vector_llm", companyId, null) : defaultAiModel;
                     String llmAnswer = multiModelRouter.generateResponse(llmPrompt, ragModel, llmSystemRole);
 
                     ragResult = llmAnswer != null && !llmAnswer.isEmpty() ?
-                            llmAnswer : "未找到相关知�?;
-                    logger.info("[RAG] 向量检索命中{}�? 来源: {}", vectorResults.size(), sources.toString());
+                            llmAnswer : "未找到相关知识";
+                    logger.info("[RAG] 向量检索命中{}条, 来源: {}", vectorResults.size(), sources.toString());
                 } else {
                     ragResult = null; // 向量无结果,走LLM兜底
                 }
             } catch (Exception e) {
                 ragResult = null;
-                logger.warn("[RAG] 向量检索失�? {}", e.getMessage());
+                logger.warn("[RAG] 向量检索失败, {}", e.getMessage());
             }
         } else {
             ragResult = null;
@@ -1231,13 +1231,13 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 fallbackVars.put("query", query);
                 String fbPrompt = promptService != null ?
                         promptService.getContent("rag_fallback_llm", fallbackVars) :
-                        ("请根据以下知识库内容回答问题,如果知识库中没有相关信息请直接说明。\n问题�? + query + "\n知识库:请参考公司内部文档和课程资料(通过语义匹配检索)");
-                String fbModel = promptService != null ? promptService.getModelName("rag_fallback_llm") : defaultAiModel;
-                String fbRole = promptService != null ? promptService.getSystemRole("rag_fallback_llm") : "你是企业知识库助手,请基于内部知识回答问�?;
+                        ("请根据以下知识库内容回答问题,如果知识库中没有相关信息请直接说明。\n问题】" + query + "\n知识库:请参考公司内部文档和课程资料(通过语义匹配检索)");
+                String fbModel = promptService != null ? promptService.getModelName("rag_fallback_llm", companyId, null) : defaultAiModel;
+                String fbRole = promptService != null ? promptService.getSystemRole("rag_fallback_llm", companyId, null) : "你是企业知识库助手,请基于内部知识回答问题";
                 String modelResult = multiModelRouter.generateResponse(fbPrompt, fbModel, fbRole);
-                ragResult = modelResult != null ? modelResult : "知识库暂无相关信�?;
+                ragResult = modelResult != null ? modelResult : "知识库暂无相关信息";
             } catch (Exception e) {
-                ragResult = "知识库检索失�? " + e.getMessage();
+                ragResult = "知识库检索失败, " + e.getMessage();
                 logger.warn("RAG_QUERY failed", e);
             }
         }
@@ -1260,7 +1260,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         if (ragResult.length() > 500)
             msg = promptService != null ?
                     promptService.getContent("kb_result_truncated", kbVars) :
-                    ("为您查询到相关信息,详情请查�?..\n" + ragResult.substring(0, Math.min(ragResult.length(), 500)) + "...");
+                    ("为您查询到相关信息,详情请查看...\n" + ragResult.substring(0, Math.min(ragResult.length(), 500)) + "...");
         deliverMessage(companyId, instance.getContactId(), channelType, msg, variables, instance.getId(), instance.getWorkflowId());
 
         logNodeExecution(companyId, instance.getId(), instance.getWorkflowId(), nextIndex, node, ragResult, query, "completed");
@@ -1270,13 +1270,13 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         result.put("nodeName", node.getNodeName());
         result.put("ragResult", ragResult);
         result.put("sources", sources.toString());
-        return AjaxResult.success("RAG检索完�?, result);
+        return AjaxResult.success("RAG检索完成", result);
     }
 
     /**
-     * CODE_EXEC 节点(type=12):代码执�?
+     * CODE_EXEC 节点(type=12):代码执
      * nodeConfig: {"language":"python|javascript|groovy", "code":"...", "timeout":10000}
-     * 安全策略:脚本内可访�?${variables} 通过变量替换注入
+     * 安全策略:脚本内可访${variables} 通过变量替换注入
      */
     private AjaxResult handleCodeExec(Long companyId, LobsterWorkflowInstance instance,
                                        CompanyWorkflowLobsterNode node, List<CompanyWorkflowLobsterNode> nodes,
@@ -1345,7 +1345,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         instance.setUpdateTime(DateUtils.getNowDate());
         instanceMapper.updateById(instance);
 
-        String msg = node.getMessageTemplate() != null ? node.getMessageTemplate() : "代码执行完成�? + execResult.substring(0, Math.min(execResult.length(), 200));
+        String msg = node.getMessageTemplate() != null ? node.getMessageTemplate() : "代码执行完成: " + execResult.substring(0, Math.min(execResult.length(), 200));
         msg = varEngine.substitute(msg, variables);
         deliverMessage(companyId, instance.getContactId(), channelType, msg, variables, instance.getId(), instance.getWorkflowId());
 
@@ -1359,9 +1359,9 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * LOOP 节点(type=13):循环迭�?
+     * LOOP 节点(type=13):循环迭
      * nodeConfig: {"loopType":"count|foreach|while", "count":3, "listVar":"items", "whileCondition":"loopCount<3", "loopNodeCode":"target_node"}
-     * 执行时推进到 loopNodeCode 指定的节点,循环完成后进�?nextNodeCode
+     * 执行时推进到 loopNodeCode 指定的节点,循环完成后进nextNodeCode
      */
     private AjaxResult handleLoop(Long companyId, LobsterWorkflowInstance instance,
                                    CompanyWorkflowLobsterNode node, List<CompanyWorkflowLobsterNode> nodes,
@@ -1420,14 +1420,14 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             instanceMapper.updateById(instance);
 
             logNodeExecution(companyId, instance.getId(), instance.getWorkflowId(), currentIndex, node,
-                    "循环�? + loopIndex + "�?, loopNodeCode, "loop_iteration");
+                    "循环(" + loopIndex + ")", loopNodeCode, "loop_iteration");
 
             Map<String, Object> result = new HashMap<>();
             result.put("instanceId", instance.getId());
             result.put("nodeName", node.getNodeName());
             result.put("loopIndex", loopIndex);
             result.put("nextNodeCode", loopNodeCode);
-            return AjaxResult.success("LOOP循环�? + loopIndex + "�?�?" + loopNodeCode, result);
+            return AjaxResult.success("LOOP循环(" + loopIndex + ")-" + loopNodeCode, result);
         }
 
         variables.remove(loopKey);
@@ -1439,13 +1439,13 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         instanceMapper.updateById(instance);
 
         logNodeExecution(companyId, instance.getId(), instance.getWorkflowId(), nextIndex, node,
-                "循环结束(�? + loopIndex + "�?", null, "completed");
+                "循环结束(" + loopIndex + ")", null, "completed");
 
         Map<String, Object> result = new HashMap<>();
         result.put("instanceId", instance.getId());
         result.put("nodeName", node.getNodeName());
         result.put("totalIterations", loopIndex);
-        return AjaxResult.success("LOOP完成(�? + loopIndex + "�?", result);
+        return AjaxResult.success("LOOP完成(" + loopIndex + ")", result);
     }
 
     private boolean evaluateWhileCondition(String condition, Map<String, Object> variables) {
@@ -1493,7 +1493,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                     return AjaxResult.success("智能API节点执行完成", apiResult);
                 }
             } catch (Exception e) {
-                logger.warn("[SmartApi] 节点 {} 模式判定失败,降�?SQL: {}", node.getId(), e.getMessage());
+                logger.warn("[SmartApi] 节点 {} 模式判定失败,降SQL: {}", node.getId(), e.getMessage());
             }
         }
 
@@ -1624,9 +1624,9 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * VARIABLE 节点(type=16):显式变量赋�?
+     * VARIABLE 节点(type=16):显式变量赋
      * nodeConfig: {"assignments":{"var1":"value1","var2":"${lastReply}"}}
-     * messageTemplate 可作为条件表达式控制是否赋�?
+     * messageTemplate 可作为条件表达式控制是否赋
      */
     private AjaxResult handleVariable(Long companyId, LobsterWorkflowInstance instance,
                                        CompanyWorkflowLobsterNode node, List<CompanyWorkflowLobsterNode> nodes,
@@ -1663,12 +1663,12 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         Map<String, Object> result = new HashMap<>();
         result.put("instanceId", instance.getId());
         result.put("nodeName", node.getNodeName());
-        return AjaxResult.success("变量赋值完�?, result);
+        return AjaxResult.success("变量赋值完成", result);
     }
 
     /**
-     * TAG_OPERATION 节点(type=9):标签操�?
-     * nodeConfig: {"addTags":["意向客户","高净�?],"removeTags":["沉睡客户"]}
+     * TAG_OPERATION 节点(type=9):标签操
+     * nodeConfig: {"addTags":["意向客户","高净值"],"removeTags":["沉睡客户"]}
      */
     private AjaxResult handleTagOperation(Long companyId, LobsterWorkflowInstance instance,
                                            CompanyWorkflowLobsterNode node, List<CompanyWorkflowLobsterNode> nodes,
@@ -1713,8 +1713,8 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
     }
 
     /**
-     * 动态AI兜底:未知节点类型自动通过LLM推导执行逻辑并生成结�?
-     * 这是"即插即用"的关键——任何新节点类型无需改代码即可运�?
+     * 动态AI兜底:未知节点类型自动通过LLM推导执行逻辑并生成结
+     * 这是"即插即用"的关键——任何新节点类型无需改代码即可运
      */
     private AjaxResult handleUnknownNodeDynamically(Long companyId, LobsterWorkflowInstance instance,
                                                       CompanyWorkflowLobsterNode node, List<CompanyWorkflowLobsterNode> nodes,
@@ -1724,17 +1724,17 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         java.util.Map<String, String> dynVars = new java.util.HashMap<>();
         dynVars.put("nodeName", node.getNodeName());
         dynVars.put("nodeType", String.valueOf(node.getNodeType()));
-        dynVars.put("messageTemplate", node.getMessageTemplate() != null ? node.getMessageTemplate() : "�?);
+        dynVars.put("messageTemplate", node.getMessageTemplate() != null ? node.getMessageTemplate() : "");
         dynVars.put("config", nodeConfig);
         dynVars.put("variables", JSON.toJSONString(variables));
         String prompt = promptService != null ?
                 promptService.getContent("unknown_node_dynamic", companyId, null, dynVars) :
                 ("你是一个CRM工作流引擎。请根据节点配置生成执行结果文本。\n节点名称: " + node.getNodeName() + "\n节点类型: " + node.getNodeType() + "...");
-        String dynModel = promptService != null ? promptService.getModelName("unknown_node_dynamic") : defaultAiModel;
+        String dynModel = promptService != null ? promptService.getModelName("unknown_node_dynamic", companyId, null) : defaultAiModel;
 
         String dynamicResult;
         try {
-            // 节点�?sceneCode/modelName 优先(user 在画布上指定�?admin/shezhi/aiModel 场景�?
+            // 节点取sceneCode/modelName 优先(user 在画布上指定,admin/shezhi/aiModel 场景下)
             String nodeScene = node.getSceneCode();
             String nodeModel = node.getModelName();
             if ((nodeScene != null && !nodeScene.isEmpty()) || (nodeModel != null && !nodeModel.isEmpty())) {
@@ -1745,10 +1745,10 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 dynamicResult = multiModelRouter.generateResponse(prompt, dynModel, null);
             }
             if (dynamicResult == null || dynamicResult.isEmpty()) {
-                dynamicResult = "节点[" + node.getNodeName() + "]已执行完�?type=" + node.getNodeType() + ")";
+                dynamicResult = "节点[" + node.getNodeName() + "]已执行完成(type=" + node.getNodeType() + ")";
             }
         } catch (Exception e) {
-            dynamicResult = "节点[" + node.getNodeName() + "]已执�?类型" + node.getNodeType() + "当前不支持,使用默认逻辑)";
+            dynamicResult = "节点[" + node.getNodeName() + "]已执行(类型" + node.getNodeType() + "当前不支持,使用默认逻辑)";
         }
 
         variables.put("dynamicResult", dynamicResult);
@@ -1773,9 +1773,9 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
         return AjaxResult.success("动态执行完成[type=" + node.getNodeType() + "]", result);
     }
 
-    // ══════════════════════════════════════════�?
-    //  工作流模拟执�?
-    // ══════════════════════════════════════════�?
+    // ══════════════════════════════════════════
+    //  工作流模拟执
+    // ══════════════════════════════════════════
 
     @Override
     public Map<String, Object> simulateExecution(Long companyId, Long workflowId,
@@ -1807,8 +1807,8 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 return report;
             }
 
-            // 逐节点模拟推�?
-            int maxSteps = nodes.size() * 3; // 最�?倍节点数�?
+            // 逐节点模拟推
+            int maxSteps = nodes.size() * 3; // 最多3倍节点数防止
             String currentNodeCode = nodes.get(0).getNodeCode();
             int steps = 0;
 
@@ -1816,7 +1816,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 steps++;
                 CompanyWorkflowLobsterNode node = findNodeByCode(nodes, currentNodeCode);
                 if (node == null) {
-                    failures.add(Map.of("nodeCode", currentNodeCode, "reason", "节点编码不存�?));
+                    failures.add(Map.of("nodeCode", currentNodeCode, "reason", "节点编码不存在"));
                     break;
                 }
 
@@ -1828,7 +1828,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
                 // 生成模拟客户回复
                 String mockReply = generateMockReply(nodeType, message, simVars, mockCustomerProfile);
 
-                // 条件判断节点:模拟条件匹�?
+                // 条件判断节点:模拟条件匹
                 if (nodeType == 3 && node.getConditionExpr() != null) {
                     try {
                         JSONObject cond = JSON.parseObject(node.getConditionExpr());
@@ -1868,7 +1868,7 @@ public class LobsterWorkflowExecutorImpl implements LobsterWorkflowExecutor {
             }
 
             if (steps >= maxSteps) {
-                failures.add(Map.of("nodeCode", currentNodeCode != null ? currentNodeCode : "END", "reason", "超过最大模拟步数,疑似死循�?));
+                failures.add(Map.of("nodeCode", currentNodeCode != null ? currentNodeCode : "END", "reason", "超过最大模拟步数,疑似死循环"));
             }
         } catch (Exception e) {
             logger.warn("模拟执行失败: {}", e.getMessage());

+ 58 - 57
fs-service/src/main/java/com/fs/company/service/workflow/impl/MultiModelWorkflowGeneratorImpl.java

@@ -16,6 +16,7 @@ import com.fs.company.mapper.LobsterAuxiliaryMapper;
 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.*;
@@ -27,7 +28,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
     private static final Logger logger = LoggerFactory.getLogger(MultiModelWorkflowGeneratorImpl.class);
 
-    /** 工作流生成场景编�?*/
+    /** 工作流生成场景编*/
     private static final String SCENE_WORKFLOW_GENERATION = "workflow_generation";
 
     @Autowired
@@ -57,14 +58,14 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
     /* ============ 行业场景规则 ============ */
     private static final Map<String, String> INDUSTRY_RULES = new LinkedHashMap<>();
     static {
-        INDUSTRY_RULES.put("travel", "旅游行业:节点序�?START→AI识别→信息收�?目的�?预算/人数/日期)→发送方案→等待回复→条件判断→创建跟进/转人工→END。话术应包含线路推荐、价格说明、优惠活动。判断条件包含customerIntent=purchase|inquiry�?);
-        INDUSTRY_RULES.put("medical", "医美行业:节点序�?START→AI识别→信息收�?项目意向/预算/时间)→发送方案→等待回复→条件判断→创建跟进→END。话术需含合规免责声明。判断条件检测sentiment<0时转人工�?);
-        INDUSTRY_RULES.put("education", "教育行业:节点序�?START→AI识别→信息收�?年龄/科目/目标)→发送试听→等待回复→条件判断→创建跟进→END。话术包含课程特色、师资介绍�?);
-        INDUSTRY_RULES.put("insurance", "保险行业:节点序�?START→AI识别→信息收�?险种/预算/年龄)→发送方案→等待回复→条件判断→创建跟进→END。话术必须包含合规免责声明,禁止承诺收益�?);
-        INDUSTRY_RULES.put("general", "通用行业:节点序�?START→AI识别→信息收集→发送消息→等待回复→条件判断→创建跟进→END。话术应友好专业�?);
+        INDUSTRY_RULES.put("travel", "旅游行业:节点序号START→AI识别→信息收集(目的地/预算/人数/日期)→发送方案→等待回复→条件判断→创建跟进/转人工→END。话术应包含线路推荐、价格说明、优惠活动。判断条件包含customerIntent=purchase|inquiry");
+        INDUSTRY_RULES.put("medical", "医美行业:节点序号START→AI识别→信息收集(项目意向/预算/时间)→发送方案→等待回复→条件判断→创建跟进→END。话术需含合规免责声明。判断条件检测sentiment<0时转人工");
+        INDUSTRY_RULES.put("education", "教育行业:节点序号START→AI识别→信息收集(年龄/科目/目标)→发送试听→等待回复→条件判断→创建跟进→END。话术包含课程特色、师资介绍");
+        INDUSTRY_RULES.put("insurance", "保险行业:节点序号START→AI识别→信息收集(险种/预算/年龄)→发送方案→等待回复→条件判断→创建跟进→END。话术必须包含合规免责声明,禁止承诺收益");
+        INDUSTRY_RULES.put("general", "通用行业:节点序START→AI识别→信息收集→发送消息→等待回复→条件判断→创建跟进→END。话术应友好专业");
     }
 
-    /** 行业规则加载:DB promptService > 硬编码降�?*/
+    /** 行业规则加载:DB promptService > 硬编码降*/
     private String getIndustryRule(Long companyId, String industryType) {
         if (promptService != null && industryType != null) {
             try {
@@ -104,11 +105,11 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
                 ? sceneService.getEnabledModels(SCENE_WORKFLOW_GENERATION)
                 : Collections.emptyList();
 
-            // 构建3个阶段的提示�?
+            // 构建3个阶段的提示
             List<String> prompts = new ArrayList<>();
             prompts.add(buildGeneratePrompt(requirement, industryType, dynamicNodeTypes, industryRule));
-            prompts.add(buildImprovePrompt(requirement, dynamicNodeTypes, "【占�?将替换为模型A输出�?));
-            prompts.add(buildValidatePrompt(dynamicNodeTypes, "【占�?将替换为模型B输出�?));
+            prompts.add(buildImprovePrompt(requirement, dynamicNodeTypes, "【占位符-将替换为模型A输出】"));
+            prompts.add(buildValidatePrompt(dynamicNodeTypes, "【占位符-将替换为模型B输出】"));
 
             List<String> systemPrompts = Arrays.asList(
                 "workflow_generator", "workflow_improver", "workflow_validator");
@@ -117,8 +118,8 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
             // 如果有pipeline引擎且模型≥1,使用流水线引擎
             if (pipelineEngine != null && models.size() >= 3) {
-                // 为阶�?�?构建正确的prompt(需要阶�?的输出,这里先用初始prompt�?
-                // 流水线引擎会自动将当前输出作为后续阶段的上下�?
+                // 为阶段2/3构建正确的prompt(需要阶段1的输出,这里先用初始prompt)
+                // 流水线引擎会自动将当前输出作为后续阶段的上下
                 MultiModelPipelineEngine.SequentialPipelineResult pipeResult =
                     pipelineEngine.executeSequential(models, prompts, systemPrompts, fallback);
 
@@ -135,7 +136,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
                 autoGeneratePrompts(companyId, industryType, finalWorkflow, requirement);
                 return GenerationResult.success(finalWorkflow, modelAOutput, modelBOutput,
-                    "Pipeline executed", "85", "流水线模式完�?);
+                    "Pipeline executed", "85", "流水线模式完成");
             }
 
             // ── 降级:单模型场景或旧模式 ──
@@ -173,7 +174,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
         } catch (Exception e) {
             logger.error("[MultiModelWorkflow] Multi-model generation failed: {}", e.getMessage(), e);
-            return GenerationResult.fail("多模态生成失�? " + e.getMessage());
+            return GenerationResult.fail("多模态生成失 " + e.getMessage());
         }
     }
 
@@ -188,14 +189,14 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
             String dynamicNodeTypes = buildDynamicNodeTypeList();
 
             String prompt = "你是一个工作流优化专家。请根据修改指令对现有工作流进行增量修改。\n\n" +
-                    "现有工作�?JSON:\n" + existingWorkflowJson + "\n\n" +
+                    "现有工作JSON:\n" + existingWorkflowJson + "\n\n" +
                     "修改指令: " + modifyInstruction + "\n\n" +
-                    "可用的节点类�?\n" + dynamicNodeTypes + "\n\n" +
-                    "要求: 1.只修改指令要求的部分 2.保持其他节点不变 3.确保节点编码一致�?4.输出纯JSON";
+                    "可用的节点类型:\n" + dynamicNodeTypes + "\n\n" +
+                    "要求: 1.只修改指令要求的部分 2.保持其他节点不变 3.确保节点编码一致4.输出纯JSON";
 
             String improved = sceneDispatcher.dispatch(prompt, SCENE_WORKFLOW_GENERATION, "workflow_optimizer");
             if (improved == null || improved.isEmpty()) {
-                return GenerationResult.fail("AI迭代优化未产生结�?);
+                return GenerationResult.fail("AI迭代优化未产生结果");
             }
 
             improved = repairJson(improved);
@@ -213,7 +214,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
         }
     }
 
-    /* ============ DB驱动的节点类型列表构�?============ */
+    /* ============ DB驱动的节点类型列表构============ */
     private String buildDynamicNodeTypeList() {
         if (nodeTypeService == null || nodeTypeService.getAllEnabled() == null) {
             return getFallbackNodeTypes();
@@ -235,23 +236,23 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
     }
 
     private String getFallbackNodeTypes() {
-        return "1-START(开�?, 2-AI_PROCESS(AI处理), 3-SEND_MESSAGE(发送消�?, " +
+        return "1-START(开始), 2-AI_PROCESS(AI处理), 3-SEND_MESSAGE(发送消息), " +
                 "4-WAIT(等待), 5-CONDITION(条件判断), 6-TASK(创建任务), " +
-                "7-COLLECT_INFO(信息收集), 8-TRANSFER_HUMAN(转人�?, " +
+                "7-COLLECT_INFO(信息收集), 8-TRANSFER_HUMAN(转人工), " +
                 "9-TAG_OPERATION(标签操作), 10-HTTP_CALL(HTTP调用), " +
-                "11-RAG_QUERY(知识检�?, 12-CODE_EXEC(代码执行), " +
-                "13-LOOP(循环迭代), 14-DB_QUERY(数据库查�?, " +
-                "15-SUB_WORKFLOW(子流�?, 16-VARIABLE(变量赋�?, 99-END(结束)";
+                "11-RAG_QUERY(知识检索), 12-CODE_EXEC(代码执行), " +
+                "13-LOOP(循环迭代), 14-DB_QUERY(数据库查询), " +
+                "15-SUB_WORKFLOW(子流程), 16-VARIABLE(变量赋值), 99-END(结束)";
     }
 
-    /* ============ JSON鲁棒性治�?============ */
+    /* ============ JSON鲁棒性治============ */
     public static String repairJson(String raw) {
         if (raw == null || raw.isEmpty()) return "{}";
 
-        // 1. 去除markdown代码块包�?
+        // 1. 去除markdown代码块包
         raw = raw.replaceAll("^```(json|JSON)?\\s*\\n?", "").replaceAll("\\n?```\\s*$", "");
 
-        // 2. 去除AI前缀废话(以非{开头的内容�?
+        // 2. 去除AI前缀废话(以非{开头的内容
         int braceStart = raw.indexOf('{');
         int bracketStart = raw.indexOf('[');
         int start = braceStart >= 0 ? braceStart : bracketStart;
@@ -261,13 +262,13 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
         // 3. 自动补全截断的JSON
         raw = autoCompleteJson(raw);
 
-        // 4. 修复未加引号的字符串�?
+        // 4. 修复未加引号的字符串
         raw = fixUnquotedValues(raw);
 
         // 5. 修复尾部逗号
         raw = raw.replaceAll(",\\s*}", "}").replaceAll(",\\s*]", "]");
 
-        // 6. 验证有效�?
+        // 6. 验证有效
         try {
             JSON.parse(raw);
             return raw;
@@ -285,7 +286,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
                 JSON.parse(retry);
                 return retry;
             } catch (Exception e2) {
-                logger.warn("[JSON修复] 最终修复失�? {}", e2.getMessage());
+                logger.warn("[JSON修复] 最终修复失 {}", e2.getMessage());
                 // 提取已解析的部分
                 return extractPartialJson(raw);
             }
@@ -331,15 +332,15 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
         return "{\"templateName\":\"修复的工作流\",\"nodes\":[{\"nodeCode\":\"START\",\"nodeName\":\"开始\",\"nodeType\":1,\"sortNo\":1,\"nextNodeCode\":\"END\"},{\"nodeCode\":\"END\",\"nodeName\":\"结束\",\"nodeType\":99,\"sortNo\":2}]}";
     }
 
-    /* ============ 提示词构建方法(纯字符串构建,不调用模型�?============ */
+    /* ============ 提示词构建方法(纯字符串构建,不调用模型============ */
 
     private String buildGeneratePrompt(String requirement, String industryType,
                                         String dynamicNodeTypes, String industryRule) {
         return "你是CRM系统专家级工作流设计师。根据需求描述和行业规则,生成完整的工作流模板。\n\n" +
                 "【需求描述】\n" + (requirement != null ? requirement : "通用客户跟进流程") + "\n\n" +
-                "【行业规�?- 必须遵守】\n" + industryRule + "\n\n" +
-                "【可用节点类�?- 必须使用这些数字】\n" + dynamicNodeTypes + "\n\n" +
-                "【输出格�?- 纯JSON】\n" +
+                "【行业规- 必须遵守】\n" + industryRule + "\n\n" +
+                "【可用节点类- 必须使用这些数字】\n" + dynamicNodeTypes + "\n\n" +
+                "【输出格- 纯JSON】\n" +
                 "{\"templateName\":\"工作流名称\",\"industryType\":\"行业代码\",\"description\":\"描述\",\n" +
                 " \"variables\":[{\"name\":\"变量名\",\"label\":\"标签\",\"type\":\"string|number|list\"}],\n" +
                 " \"nodes\":[\n" +
@@ -350,23 +351,23 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
                 " ],\n" +
                 " \"edges\":[{\"sourceNodeCode\":\"源\",\"targetNodeCode\":\"目标\",\"edgeLabel\":\"标签\"}]\n" +
                 "}\n\n" +
-                "要求: 1.最�?个节�?2.nodeCode唯一 3.最后一个节点nextNodeCode为空 4.输出纯JSON无其他文�?;
+                "要求: 1.最少3个节点 2.nodeCode唯一 3.最后一个节点nextNodeCode为空 4.输出纯JSON无其他文字";
     }
 
     private String buildImprovePrompt(String requirement, String dynamicNodeTypes, String draftJson) {
-        return "你是工作流优化专家。审查并完善这个工作流草稿,检�?\n" +
-                "1. 节点连接是否合理 2. 话术模板是否完整 3. 条件表达式是否正�?4. 变量是否定义\n\n" +
-                "原始需�? " + (requirement != null ? requirement : "") + "\n\n" +
+        return "你是工作流优化专家。审查并完善这个工作流草稿,检\n" +
+                "1. 节点连接是否合理 2. 话术模板是否完整 3. 条件表达式是否正4. 变量是否定义\n\n" +
+                "原始需 " + (requirement != null ? requirement : "") + "\n\n" +
                 "可用节点类型: " + dynamicNodeTypes + "\n\n" +
-                "工作流草�?\n" + draftJson + "\n\n" +
+                "工作流草稿\n" + draftJson + "\n\n" +
                 "输出纯JSON(只输出改进后的工作流,无其他文字):";
     }
 
     private String buildValidatePrompt(String dynamicNodeTypes, String workflowJson) {
         return "你是工作流QA专家。验证以下工作流JSON:\n\n" +
                 "期望节点类型: " + dynamicNodeTypes + "\n\n" +
-                "工作�?\n" + workflowJson + "\n\n" +
-                "检�? 1.节点类型是否在可用范围内 2.nodeCode是否唯一 3.节点是否连�?4.START/END是否存在\n" +
+                "工作\n" + workflowJson + "\n\n" +
+                "检查 1.节点类型是否在可用范围内 2.nodeCode是否唯一 3.节点是否连通4.START/END是否存在\n" +
                 "输出JSON: {\"passed\":true/false,\"score\":\"0-100\",\"suggestions\":[\"建议1\"],\"details\":\"详情\"}";
     }
 
@@ -385,7 +386,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
     private String buildDefaultWorkflow(String requirement, String industryType) {
         Map<String, Object> result = new HashMap<>();
-        result.put("templateName", "AI生成工作流方�?);
+        result.put("templateName", "AI生成工作流方案");
         result.put("industryType", industryType != null ? industryType : "general");
         result.put("description", requirement != null ? requirement : "");
 
@@ -398,7 +399,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
         List<Map<String, Object>> nodes = new ArrayList<>();
         Map<String, Object> start = new HashMap<>();
-        start.put("nodeCode", "START"); start.put("nodeName", "开始节�?);
+        start.put("nodeCode", "START"); start.put("nodeName", "开始节点");
         start.put("nodeType", 1); start.put("sortNo", 1);
         start.put("nextNodeCode", "MSG_1"); start.put("messageTemplate", "");
         start.put("conditionExpr", ""); start.put("nodeConfig", "{}"); start.put("maxRounds", 0);
@@ -422,7 +423,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
         List<Map<String, Object>> edges = new ArrayList<>();
         Map<String, Object> e1 = new HashMap<>();
-        e1.put("sourceNodeCode", "START"); e1.put("targetNodeCode", "MSG_1"); e1.put("edgeLabel", "开�?);
+        e1.put("sourceNodeCode", "START"); e1.put("targetNodeCode", "MSG_1"); e1.put("edgeLabel", "开始");
         edges.add(e1);
         Map<String, Object> e2 = new HashMap<>();
         e2.put("sourceNodeCode", "MSG_1"); e2.put("targetNodeCode", "END"); e2.put("edgeLabel", "结束");
@@ -435,8 +436,8 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
     /* ============ 自动Prompt生成 ============ */
 
     /**
-     * AI生成workflow后,自动为租�?行业创建专属Prompt
-     * 从生成的节点中提取消息模板、条件表达式 �?写入lobster_system_prompt
+     * AI生成workflow后,自动为租户/行业创建专属Prompt
+     * 从生成的节点中提取消息模板、条件表达式写入lobster_system_prompt
      */
     private void autoGeneratePrompts(Long companyId, String industryType, String workflowJson, String requirement) {
         if (auxMapper == null || companyId == null) return;
@@ -461,14 +462,14 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
                 String nodeName = node.getString("nodeName");
                 String msgTemplate = node.getString("messageTemplate");
 
-                /* 提取消息节点的话术作为参�?*/
+                /* 提取消息节点的话术作为参*/
                 if (msgTemplate != null && !msgTemplate.isEmpty() &&
                     (nodeType == 3 || nodeType == 7 || nodeType == 8)) {
                     contextMessages.append("[").append(nodeName).append("]: ")
                             .append(msgTemplate).append("\n");
                 }
 
-                /* 提取信息收集节点的字�?*/
+                /* 提取信息收集节点的字*/
                 if (nodeType != null && nodeType == 7) {
                     String cfg = node.getString("nodeConfig");
                     if (cfg != null && !cfg.isEmpty()) {
@@ -484,7 +485,7 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
                     }
                 }
 
-                /* 提取条件表达�?*/
+                /* 提取条件表达*/
                 String condExpr = node.getString("conditionExpr");
                 if (condExpr != null && !condExpr.isEmpty()) {
                     condRules.append("[").append(nodeName).append("]: ").append(condExpr).append("\n");
@@ -494,31 +495,31 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
             /* 1. 写入行业规则(tenant+industry) */
             String ruleKey = "industry_rule_" + ind;
             String ruleContent = "行业[" + ind + "]自动生成规则: " + (requirement != null ? requirement : "") +
-                    ". 节点话术参�?\n" + contextMessages.toString();
+                    ". 节点话术参\n" + contextMessages.toString();
             upsertPrompt(ruleKey, ind + "行业规则(自动)", "industry", ruleContent, companyId, ind, "deepseek");
 
-            /* 2. 生成信息收集提示�?*/
+            /* 2. 生成信息收集提示*/
             if (collectMessages.length() > 0) {
-                String collectContent = "自动收集提示: 询问用户�? + collectMessages.toString();
+                String collectContent = "自动收集提示: 询问用户关于" + collectMessages.toString();
                 upsertPrompt("collect_info_question", "信息收集提问(自动)", "collect",
                         collectContent, companyId, ind, "doubao-lite");
             }
 
-            /* 3. 生成条件判断提示�?*/
+            /* 3. 生成条件判断提示*/
             if (condRules.length() > 0) {
                 upsertPrompt("condition_rules", "条件判断规则(自动)", "condition",
                         condRules.toString(), companyId, ind, "deepseek");
             }
 
-            /* 4. 生成通用AI系统提示�?*/
-            String aiPrompt = "你是" + ind + "行业的AI销售助手,工作流模�? " + templateName +
-                    ". 核心需�? " + (requirement != null ? requirement.substring(0, Math.min(200, requirement.length())) : "通用跟进");
+            /* 4. 生成通用AI系统提示*/
+            String aiPrompt = "你是" + ind + "行业的AI销售助手,工作流模 " + templateName +
+                    ". 核心需 " + (requirement != null ? requirement.substring(0, Math.min(200, requirement.length())) : "通用跟进");
             upsertPrompt("ai_system_role", "AI系统角色(自动)", "ai",
                     aiPrompt, companyId, ind, "deepseek");
 
             /* 5. 写入租户级别Prompt(不含industry,作为该租户所有行业的兜底) */
             if (contextMessages.length() > 0) {
-                upsertPrompt("workflow_messages", "工作流话�?自动)", "msg",
+                upsertPrompt("workflow_messages", "工作流话术(自动)", "msg",
                         contextMessages.toString(), companyId, null, "deepseek");
             }
 

+ 140 - 53
fs-service/src/main/java/com/fs/company/service/workflow/impl/PendingAuditKnowledgeServiceImpl.java

@@ -1,13 +1,12 @@
 package com.fs.company.service.workflow.impl;
 
 import com.alibaba.fastjson.JSON;
-import com.fs.common.utils.DateUtils;
-import com.fs.company.domain.CompanyKnowledgeBase;
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.AjaxResult;
 import com.fs.company.dto.CompanyKnowledgeBaseDto;
 import com.fs.company.mapper.LobsterPendingKnowledgeMapper;
 import com.fs.company.service.ICompanyKnowledgeBaseService;
 import com.fs.company.service.workflow.PendingAuditKnowledgeService;
-import com.fs.company.service.workflow.QualityScoringService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -67,8 +66,8 @@ public class PendingAuditKnowledgeServiceImpl implements PendingAuditKnowledgeSe
             for (Map<String, Object> row : rows) {
                 // keyword filter in-memory
                 if (StringUtils.hasText(keyword)) {
-                    String q = (String) row.get("content");
-                    if (q == null || (!q.contains(keyword))) continue;
+                    String content = (String) row.get("content");
+                    if (content == null || (!content.contains(keyword))) continue;
                 }
                 result.add(mapToPendingKnowledge(row));
             }
@@ -101,33 +100,52 @@ public class PendingAuditKnowledgeServiceImpl implements PendingAuditKnowledgeSe
         }
     }
 
+    @Override
+    public boolean updateContent(Long companyId, Long id, String question, String answer, String userName) {
+        if (pendingKnowledgeMapper == null) return false;
+        try {
+            String q = question != null ? (question.length() > 500 ? question.substring(0, 500) : question) : "";
+            String a = answer != null ? (answer.length() > 2000 ? answer.substring(0, 2000) : answer) : "";
+            // Update by delete + re-insert (mapper doesn't have direct update)
+            pendingKnowledgeMapper.delete(id, companyId);
+            PendingKnowledge existing = getById(companyId, id);
+            String sourceType = existing != null && existing.getSourceType() != null ? existing.getSourceType() : "manual";
+            String sourceId = existing != null ? existing.getSourceId() : null;
+            pendingKnowledgeMapper.insert(companyId, null, sourceType,
+                    q + "\n---\n" + a, "{}", sourceId, "pending");
+            logger.info("[PendingAudit] Updated: id={}, companyId={}", id, companyId);
+            return true;
+        } catch (Exception e) {
+            logger.error("[PendingAudit] Update failed: {}", e.getMessage());
+            return false;
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public boolean auditApprove(Long companyId, Long id, Long auditorId, String comment) {
+    public boolean approve(Long companyId, Long id, String userName) {
         if (pendingKnowledgeMapper == null) return false;
         try {
             PendingKnowledge knowledge = getById(companyId, id);
             if (knowledge == null) return false;
 
-            pendingKnowledgeMapper.auditApprove(id, companyId, auditorId, comment);
+            pendingKnowledgeMapper.auditApprove(id, companyId, null, userName);
 
             // 自动转正到正式知识库
-            String answer = extractAnswer(knowledge.getContent());
+            String answer = knowledge.getAnswer();
             if (StringUtils.hasText(answer)) {
                 CompanyKnowledgeBaseDto dto = new CompanyKnowledgeBaseDto();
-                dto.setCompanyId(companyId);
-                dto.setTitle(knowledge.getContent() != null && knowledge.getContent().length() > 100
-                        ? knowledge.getContent().substring(0, 100) : knowledge.getContent());
+                dto.setQuestion(knowledge.getQuestion() != null && knowledge.getQuestion().length() > 100
+                        ? knowledge.getQuestion().substring(0, 100) : knowledge.getQuestion());
                 dto.setAnswer(answer);
-                dto.setType(1);
-                dto.setStatus(1);
-                knowledgeBaseService.saveKnowledge(dto, companyId);
+                knowledgeBaseService.addKnowledge(companyId, userName, dto, null);
             }
 
             // 向量化入库
             if (vectorPatternMatcher != null && StringUtils.hasText(answer)) {
                 try {
-                    vectorPatternMatcher.indexDocument(companyId, "knowledge", answer,
+                    vectorPatternMatcher.storeVector(companyId, "knowledge",
+                            "pending_audit_" + id, answer,
                             Collections.singletonMap("source", "pending_audit_" + id));
                 } catch (Exception e) { logger.debug("[PendingAudit] 向量化失败: {}", e.getMessage()); }
             }
@@ -141,10 +159,10 @@ public class PendingAuditKnowledgeServiceImpl implements PendingAuditKnowledgeSe
     }
 
     @Override
-    public boolean auditReject(Long companyId, Long id, Long auditorId, String comment) {
+    public boolean reject(Long companyId, Long id, String reason, String userName) {
         if (pendingKnowledgeMapper == null) return false;
         try {
-            pendingKnowledgeMapper.auditReject(id, companyId, auditorId, comment);
+            pendingKnowledgeMapper.auditReject(id, companyId, null, reason);
             logger.info("[PendingAudit] Rejected: id={}, companyId={}", id, companyId);
             return true;
         } catch (Exception e) {
@@ -155,42 +173,83 @@ public class PendingAuditKnowledgeServiceImpl implements PendingAuditKnowledgeSe
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public int batchApprove(Long companyId, List<Long> ids, Long auditorId) {
-        if (pendingKnowledgeMapper == null || ids == null || ids.isEmpty()) return 0;
+    public AuditResult batchApprove(Long companyId, List<Long> ids, String userName) {
+        if (pendingKnowledgeMapper == null || ids == null || ids.isEmpty()) {
+            return AuditResult.fail("无待审核记录");
+        }
         try {
-            pendingKnowledgeMapper.batchApprove(ids, companyId, auditorId);
+            pendingKnowledgeMapper.batchApprove(ids, companyId, null);
+            int approved = 0;
+            int failed = 0;
             for (Long id : ids) {
-                PendingKnowledge k = getById(companyId, id);
-                if (k != null && StringUtils.hasText(extractAnswer(k.getContent()))) {
-                    try {
+                try {
+                    PendingKnowledge k = getById(companyId, id);
+                    if (k != null && StringUtils.hasText(k.getAnswer())) {
                         CompanyKnowledgeBaseDto dto = new CompanyKnowledgeBaseDto();
-                        dto.setCompanyId(companyId);
-                        dto.setTitle(k.getContent());
-                        dto.setAnswer(extractAnswer(k.getContent()));
-                        dto.setType(1);
-                        dto.setStatus(1);
-                        knowledgeBaseService.saveKnowledge(dto, companyId);
-                    } catch (Exception ignored) {}
+                        dto.setQuestion(k.getQuestion());
+                        dto.setAnswer(k.getAnswer());
+                        AjaxResult r = knowledgeBaseService.addKnowledge(companyId, userName, dto, null);
+                        if (r != null && (int) r.get(AjaxResult.CODE_TAG) == HttpStatus.SUCCESS) {
+                            approved++;
+                        } else {
+                            failed++;
+                        }
+                    } else {
+                        approved++;
+                    }
+                } catch (Exception ignored) {
+                    failed++;
                 }
             }
-            logger.info("[PendingAudit] Batch approved: count={}, companyId={}", ids.size(), companyId);
-            return ids.size();
+            logger.info("[PendingAudit] Batch approved: approved={}, failed={}, companyId={}", approved, failed, companyId);
+            if (failed == 0) {
+                return AuditResult.success(approved);
+            } else if (approved > 0) {
+                return AuditResult.partial(approved, failed);
+            } else {
+                return AuditResult.fail("全部审核失败");
+            }
         } catch (Exception e) {
             logger.error("[PendingAudit] Batch approve failed: {}", e.getMessage());
-            return 0;
+            return AuditResult.fail(e.getMessage());
         }
     }
 
     @Override
-    public int batchReject(Long companyId, List<Long> ids, Long auditorId) {
-        if (pendingKnowledgeMapper == null || ids == null || ids.isEmpty()) return 0;
+    public boolean batchReject(Long companyId, List<Long> ids, String reason, String userName) {
+        if (pendingKnowledgeMapper == null || ids == null || ids.isEmpty()) return false;
         try {
-            pendingKnowledgeMapper.batchReject(ids, companyId, auditorId);
+            pendingKnowledgeMapper.batchReject(ids, companyId, null);
             logger.info("[PendingAudit] Batch rejected: count={}, companyId={}", ids.size(), companyId);
-            return ids.size();
+            return true;
         } catch (Exception e) {
             logger.error("[PendingAudit] Batch reject failed: {}", e.getMessage());
-            return 0;
+            return false;
+        }
+    }
+
+    @Override
+    public boolean saveDirectly(Long companyId, Long id, String userName) {
+        if (pendingKnowledgeMapper == null) return false;
+        try {
+            PendingKnowledge knowledge = getById(companyId, id);
+            if (knowledge == null) return false;
+
+            pendingKnowledgeMapper.auditApprove(id, companyId, null, "DIRECT_SAVE");
+
+            String answer = knowledge.getAnswer();
+            if (StringUtils.hasText(answer)) {
+                CompanyKnowledgeBaseDto dto = new CompanyKnowledgeBaseDto();
+                dto.setQuestion(knowledge.getQuestion());
+                dto.setAnswer(answer);
+                knowledgeBaseService.addKnowledge(companyId, userName, dto, null);
+            }
+
+            logger.info("[PendingAudit] Directly saved: id={}, companyId={}", id, companyId);
+            return true;
+        } catch (Exception e) {
+            logger.error("[PendingAudit] Direct save failed: {}", e.getMessage());
+            return false;
         }
     }
 
@@ -207,15 +266,35 @@ public class PendingAuditKnowledgeServiceImpl implements PendingAuditKnowledgeSe
     }
 
     @Override
+    public boolean batchDelete(Long companyId, List<Long> ids) {
+        if (pendingKnowledgeMapper == null || ids == null || ids.isEmpty()) return false;
+        try {
+            for (Long id : ids) {
+                pendingKnowledgeMapper.delete(id, companyId);
+            }
+            logger.info("[PendingAudit] Batch deleted: count={}, companyId={}", ids.size(), companyId);
+            return true;
+        } catch (Exception e) {
+            logger.error("[PendingAudit] Batch delete failed: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    // ========== 内部辅助方法 ==========
+
+    /**
+     * 一键转正(内部自动调用)
+     */
     public boolean promoteToKnowledge(Long companyId, Long id) {
-        return auditApprove(companyId, id, null, "AUTO_PROMOTE");
+        return approve(companyId, id, "AUTO_PROMOTE");
     }
 
-    @Override
+    /**
+     * 定时自动转正 Top 10 高质量待审核知识
+     */
     public void scheduleAutoPromote(Long companyId) {
         if (pendingKnowledgeMapper == null) return;
         try {
-            // auto-promote top 10 highest quality
             List<Map<String, Object>> rows = pendingKnowledgeMapper.selectByCompany(companyId, "pending", 0, 10);
             for (Map<String, Object> row : rows) {
                 Long id = ((Number) row.get("id")).longValue();
@@ -229,18 +308,26 @@ public class PendingAuditKnowledgeServiceImpl implements PendingAuditKnowledgeSe
         PendingKnowledge pk = new PendingKnowledge();
         pk.setId(((Number) row.get("id")).longValue());
         pk.setCompanyId(((Number) row.get("company_id")).longValue());
-        pk.setContent((String) row.get("content"));
-        pk.setKnowledgeType((String) row.get("knowledge_type"));
-        pk.setContextSnapshot((String) row.get("context_snapshot"));
-        pk.setSourceNodeCode((String) row.get("source_node_code"));
-        pk.setStatus((String) row.get("status"));
-        pk.setCreateTime(row.get("create_time") instanceof Date ? (Date) row.get("create_time") : null);
+        String content = (String) row.get("content");
+        if (content != null) {
+            int sep = content.indexOf("\n---\n");
+            if (sep >= 0) {
+                pk.setQuestion(content.substring(0, sep).trim());
+                pk.setAnswer(content.substring(sep + 5).trim());
+            } else {
+                pk.setQuestion(content);
+                pk.setAnswer("");
+            }
+        }
+        pk.setSourceType((String) row.get("knowledge_type"));
+        pk.setSourceId((String) row.get("source_node_code"));
+        pk.setAuditStatus((String) row.get("status"));
+        Object ctx = row.get("context_snapshot");
+        if (ctx instanceof String) {
+            pk.setDimensionScores((String) ctx);
+        }
+        pk.setCreateTime(row.get("create_time") instanceof Date
+                ? new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) row.get("create_time")) : null);
         return pk;
     }
-
-    private String extractAnswer(String content) {
-        if (content == null) return "";
-        int sep = content.indexOf("\n---\n");
-        return sep >= 0 ? content.substring(sep + 5).trim() : content;
-    }
 }

+ 2 - 1
fs-service/src/main/java/com/fs/company/service/workflow/impl/PromptManagerImpl.java

@@ -1,5 +1,6 @@
 package com.fs.company.service.workflow.impl;
 
+import com.fs.company.domain.LobsterSystemPrompt;
 import com.fs.company.mapper.LobsterSystemPromptMapper;
 import com.fs.company.service.workflow.PromptManager;
 import org.slf4j.Logger;
@@ -84,7 +85,7 @@ public class PromptManagerImpl implements PromptManager {
                 Map<String, Object> m = new HashMap<>();
                 m.put("id", p.getId());
                 m.put("companyId", p.getCompanyId());
-                m.put("content", p.getContent());
+                m.put("content", p.getPromptContent());
                 m.put("promptType", p.getPromptCategory());
                 result.add(m);
             }

+ 38 - 38
fs-service/src/main/java/com/fs/company/service/workflow/impl/SemanticTakeoverDetectorImpl.java

@@ -19,21 +19,21 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
 /**
- * 语义级转人工检测服务实�?
+ * 语义级转人工检测服务实
  *
  * 设计原则:无感转人工
  * ──────────────────────────────────────────────────────────────────────
- * 核心理念:不使用"转人�?�?找客�?�?真人"等暴露机器人身份的关键词�?
- * 而是通过语义分析无感地判断客户意图,以真人化话术完成转接�?
+ * 核心理念:不使用"转人工"/"找客服"/"真人"等暴露机器人身份的关键词,
+ * 而是通过语义分析无感地判断客户意图,以真人化话术完成转接
  *
  * 检测策略:
- * 1. 租户自定义关键词匹配 - 从fastgpt_chat_artificial_words表读�?
- * 2. 高风险意图检�?- 投诉/法律威胁等需立即处理的场�?
+ * 1. 租户自定义关键词匹配 - 从fastgpt_chat_artificial_words表读
+ * 2. 高风险意图检测 - 投诉/法律威胁等需立即处理的场景
  * 3. AI语义深度分析 - 秒级响应,准确率更高
- * 4. 综合决策:关键词匹配置信�?=0.7直接采用,否则取两者中置信度更高的
+ * 4. 综合决策:关键词匹配置信度>=0.7直接采用,否则取两者中置信度更高的
  *
- * ⚠️ 注意:已移除"转人�?�?找客�?�?真人"�?不要机器�?等暴露身份的关键词,
- * 改为从数据库读取租户自定义关键词,并通过语义分析检测隐含的转人工意图�?
+ * ⚠️ 注意:已移除"转人工"/"找客服"/"真人"/"不要机器人"等暴露身份的关键词,
+ * 改为从数据库读取租户自定义关键词,并通过语义分析检测隐含的转人工意图
  * ──────────────────────────────────────────────────────────────────────
  */
 @Service
@@ -54,36 +54,36 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
     private com.fs.company.mapper.LobsterAuxiliaryMapper auxMapper;
 
     /**
-     * 高风险意图关键词(投�?法律威胁等,需要立即转人工�?
-     * 这些关键词表达客户强烈不满,不会暴露机器人身�?
+     * 高风险意图关键词(投诉/法律威胁等,需要立即转人工)
+     * 这些关键词表达客户强烈不满,不会暴露机器人身
      */
     private static final String[] HIGH_RISK_KEYWORDS = {
-            "投诉", "我要投诉", "消费者协�?, "12315", "工商局",
-            "退�?, "退�?, "举报", "曝光",
+            "投诉", "我要投诉", "消费者协会", "12315", "工商局",
+            "退款", "退款", "举报", "曝光",
             "律师", "法院", "报警", "警察", "起诉"
     };
 
     /**
-     * 紧急程度关键词(中英文混合�?
-     * 客户表达急迫情绪时触发,置信�?.6,可能需要转人工
+     * 紧急程度关键词(中英文混合
+     * 客户表达急迫情绪时触发,置信度0.6,可能需要转人工
      * 不包含暴露机器人身份的词
      */
     private static final String[] URGENCY_KEYWORDS = {
             "urgent", "quick", "immediately", "now", "hurry", "fast", "rush",
-            "紧�?, "赶紧", "马上", "立刻", "现在就要", "快点", "�?,
-            "等不�?, "�?, "加�?, "十万火�?
+            "紧急", "赶紧", "马上", "立刻", "现在就要", "快点", "快",
+            "等不及", "急了", "加快", "十万火急"
     };
 
     /**
      * 客户愿意继续沟通的关键词(阻止转人工)
-     * 当客户表示愿意继续对话时,不触发转人�?
+     * 当客户表示愿意继续对话时,不触发转人
      */
     private static final String[] CONTINUE_KEYWORDS = {
             "continue", "ok", "yes", "sure", "fine",
-            "继续", "好的", "可以", "没问�?, "�?, "�?
+            "继续", "好的", "可以", "没问题", "行", "好"
     };
 
-    /** 租户关键词缓�?*/
+    /** 租户关键词缓*/
     private final Map<Long, List<String>> tenantKeywordCache = new ConcurrentHashMap<>();
     private final Map<Long, Long> keywordCacheRefreshTime = new ConcurrentHashMap<>();
     private static final long CACHE_TTL_MS = 5 * 60 * 1000L;
@@ -94,13 +94,13 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
      * 无感转人工策略:
      * - 优先匹配租户自定义关键词(从fastgpt_chat_artificial_words表读取)
      * - 高风险意图直接触发(投诉/法律威胁等)
-     * - 不使�?转人�?�?找客�?等暴露身份的关键�?
-     * - 转人工话术使用IdentityHidingService生成真人化表�?
+     * - 不使用"转人工"/"找客服"等暴露身份的关键词
+     * - 转人工话术使用IdentityHidingService生成真人化表
      *
      * @param companyId      租户ID
      * @param customerMessage 客户消息内容
-     * @param context         对话上下�?
-     * @return 转人工检测结�?
+     * @param context         对话上下
+     * @return 转人工检测结
      */
     @Override
     public TakeoverResult detectTakeover(Long companyId, String customerMessage, String context) {
@@ -116,7 +116,7 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
             return tenantResult;
         }
 
-        // 第二步:高风险意图检�?
+        // 第二步:高风险意图检
         TakeoverResult highRiskResult = detectByHighRiskKeywords(customerMessage);
         if (highRiskResult.isShouldTakeover() && highRiskResult.getConfidence() >= 0.7) {
             return highRiskResult;
@@ -138,11 +138,11 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
 
     /**
      * 租户自定义关键词匹配
-     * 从fastgpt_chat_artificial_words表读取租户配置的触发关键�?
+     * 从fastgpt_chat_artificial_words表读取租户配置的触发关键
      *
      * @param companyId      租户ID
      * @param customerMessage 客户消息
-     * @return 关键词匹配结�?
+     * @return 关键词匹配结
      */
     private TakeoverResult detectByTenantKeywords(Long companyId, String customerMessage) {
         TakeoverResult result = new TakeoverResult();
@@ -205,9 +205,9 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
     }
 
     /**
-     * 获取租户自定义关键词列表(带缓存�?
-     * 从fastgpt_chat_artificial_words表读取type=2(关键词类型)且status=0(启用)的记�?
-     * 同时加载租户专属关键�?company_id=租户ID)和全局共享关键�?company_id IS NULL)
+     * 获取租户自定义关键词列表(带缓存
+     * 从fastgpt_chat_artificial_words表读取type=2(关键词类型)且status=0(启用)的记
+     * 同时加载租户专属关键词(company_id=租户ID)和全局共享关键词(company_id IS NULL)
      */
     private List<String> getTenantKeywords(Long companyId) {
         Long lastRefresh = keywordCacheRefreshTime.get(companyId);
@@ -246,7 +246,7 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
                         .collect(Collectors.toList());
             }
         } catch (Exception e) {
-            logger.warn("[SemanticTakeover] 加载租户关键词失�? companyId={}", companyId);
+            logger.warn("[SemanticTakeover] 加载租户关键词失 companyId={}", companyId);
         }
 
         tenantKeywordCache.put(companyId, keywords);
@@ -255,15 +255,15 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
     }
 
     /**
-     * AI语义深度分析转人工意�?
+     * AI语义深度分析转人工意
      * 使用AI模型分析客户消息的深层语义,判断是否需要转人工
      * 当关键词匹配无法确定时使用,准确率更高但延迟更大
      *
-     * 提示词设计:不使�?转人�?等暴露身份的措辞,改�?需要更专业的帮�?
+     * 提示词设计:不使用"转人工"等暴露身份的措辞,改为"需要更专业的帮助"
      *
      * @param companyId      租户ID
      * @param customerMessage 客户消息
-     * @param context         对话上下�?
+     * @param context         对话上下
      * @return AI分析结果,失败时返回null(降级为关键词结果)
      */
     private TakeoverResult detectByLLM(Long companyId, String customerMessage, String context) {
@@ -279,9 +279,9 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
     }
 
     /**
-     * 解析AI返回的JSON格式转人工检测结�?
+     * 解析AI返回的JSON格式转人工检测结
      *
-     * @param aiResponse AI返回的JSON字符�?
+     * @param aiResponse AI返回的JSON字符
      * @return 解析后的结果,解析失败返回null
      */
     private TakeoverResult parseLLMResult(String aiResponse) {
@@ -304,13 +304,13 @@ public class SemanticTakeoverDetectorImpl implements SemanticTakeoverDetector {
     }
 
     /**
-     * 检测客户是否愿意继续沟�?
+     * 检测客户是否愿意继续沟
      * 当客户明确表示愿意继续时,阻止转人工
      *
      * @param companyId      租户ID
      * @param customerMessage 客户消息
-     * @param context         对话上下�?
-     * @return 检测结�?
+     * @param context         对话上下
+     * @return 检测结
      */
     @Override
     public TakeoverResult detectAITransfer(Long companyId, String customerMessage, String context) {

+ 13 - 0
fs-service/src/main/java/com/fs/company/service/workflow/impl/SensitiveWordServiceImpl.java

@@ -83,6 +83,19 @@ public class SensitiveWordServiceImpl implements SensitiveWordService {
         return pos;
     }
 
+    @Override
+    public boolean isHighRiskSensitiveWord(String content, Long companyId) {
+        if (content == null || content.isEmpty()) return false;
+        List<SensitiveWordEntry> words = getSensitiveWords(companyId);
+        for (SensitiveWordEntry entry : words) {
+            if (content.contains(entry.word) && ("法律".equals(entry.category) ||
+                    "政治".equals(entry.category) || "诈骗".equals(entry.category))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     @Override
     public void addSensitiveWord(Long companyId, String word, String replacement, String category) {
         if (sensitiveWordMapper == null) return;

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/impl/SummaryGeneratorImpl.java

@@ -368,7 +368,7 @@ public class SummaryGeneratorImpl implements SummaryGenerator {
             String line = lines[i].trim();
             if (line.isEmpty()) continue;
             // 简单提取关键词(中文字符/英文单词长度≥2)
-            String[] words = line.replaceAll("[\\p{Punct},。!?、:;""''()]", " ").split("\\s+");
+            String[] words = line.replaceAll("[\\p{Punct},。!?、:;''()]", " ").split("\\s+");
             for (String w : words) {
                 if (w.length() >= 2 && !"客户".equals(w) && !"AI".equals(w)) keywords.add(w);
             }
@@ -386,7 +386,7 @@ public class SummaryGeneratorImpl implements SummaryGenerator {
             if (summaries == null || summaries.isEmpty()) return "暂无全局摘要";
             StringBuilder sb = new StringBuilder();
             for (LobsterConversationSummary s : summaries) {
-                sb.append(s.getSummaryType()).append(": ").append(s.getSummaryContent()).append("\n");
+                sb.append(s.getSummaryText()).append("\n");
             }
             if (sb.length() < 500) return sb.toString();
             String prompt = "合并以下客户历史摘要为一段150字以内的全局画像摘要:\n" + sb +

+ 103 - 17
fs-service/src/main/java/com/fs/company/service/workflow/knowledge/impl/KnowledgeVersionManagerImpl.java

@@ -1,6 +1,7 @@
 package com.fs.company.service.workflow.knowledge.impl;
 
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.fs.company.service.workflow.knowledge.KnowledgeVersionManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -17,41 +18,126 @@ public class KnowledgeVersionManagerImpl implements com.fs.company.service.workf
     private LobsterAuxiliaryMapper auxMapper;
 
     @Override
-    public int createVersion(Long companyId, Long knowledgeId, String title, String content, String changeLog) {
+    public int createVersion(Long companyId, Long knowledgeId, String changeType, String changeReason, String changedBy) {
         if (auxMapper == null) return 0;
         try {
+            Map<String, Object> knowledge = auxMapper.selectKnowledgeById(companyId, knowledgeId);
+            String title = knowledge != null ? (String) knowledge.getOrDefault("title", "Knowledge#" + knowledgeId) : "Knowledge#" + knowledgeId;
+            String content = knowledge != null ? (String) knowledge.getOrDefault("content", "") : "";
             int maxVersion = auxMapper.selectMaxVersion(companyId, knowledgeId);
             int newVersion = maxVersion + 1;
-            auxMapper.insertVersion(companyId, knowledgeId, newVersion, title, content, changeLog);
+            auxMapper.insertVersion(companyId, knowledgeId, newVersion, title, content,
+                    changeType + ":" + (changeReason != null ? changeReason : "") + " by " + (changedBy != null ? changedBy : "system"));
             return newVersion;
-        } catch (Exception e) { return 0; }
+        } catch (Exception e) {
+            logger.error("[KnowledgeVersion] createVersion failed: {}", e.getMessage());
+            return 0;
+        }
     }
 
     @Override
-    public List<Map<String, Object>> getVersions(Long companyId, Long knowledgeId) {
-        if (auxMapper == null) return new ArrayList<>();
-        try { return auxMapper.selectVersions(companyId, knowledgeId); }
-        catch (Exception e) { return new ArrayList<>(); }
+    public List<KnowledgeVersion> getVersionHistory(Long companyId, Long knowledgeId) {
+        List<KnowledgeVersion> result = new ArrayList<>();
+        if (auxMapper == null) return result;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectVersions(companyId, knowledgeId);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    result.add(mapToKnowledgeVersion(row));
+                }
+            }
+        } catch (Exception e) {
+            logger.error("[KnowledgeVersion] getVersionHistory failed: {}", e.getMessage());
+        }
+        return result;
     }
 
     @Override
-    public Map<String, Object> getVersion(Long companyId, Long knowledgeId, int version) {
+    public KnowledgeVersion getVersion(Long companyId, Long knowledgeId, int version) {
         if (auxMapper == null) return null;
-        try { return auxMapper.selectVersion(companyId, knowledgeId, version); }
-        catch (Exception e) { return null; }
+        try {
+            Map<String, Object> row = auxMapper.selectVersion(companyId, knowledgeId, version);
+            if (row != null) {
+                return mapToKnowledgeVersion(row);
+            }
+        } catch (Exception e) {
+            logger.error("[KnowledgeVersion] getVersion failed: {}", e.getMessage());
+        }
+        return null;
     }
 
     @Override
-    public boolean rollback(Long companyId, Long knowledgeId, int toVersion) {
-        if (auxMapper == null) return false;
-        try { auxMapper.rollbackVersion(companyId, knowledgeId, toVersion); return true; }
-        catch (Exception e) { return false; }
+    public VersionDiff diffVersions(Long companyId, Long knowledgeId, int fromVersion, int toVersion) {
+        VersionDiff diff = new VersionDiff();
+        diff.setFromVersion(fromVersion);
+        diff.setToVersion(toVersion);
+        if (auxMapper == null) {
+            diff.setSummary("Mapper unavailable, cannot compare");
+            return diff;
+        }
+        try {
+            Map<String, Object> from = auxMapper.selectVersion(companyId, knowledgeId, fromVersion);
+            Map<String, Object> to = auxMapper.selectVersion(companyId, knowledgeId, toVersion);
+            List<FieldDiff> fields = new ArrayList<>();
+            if (from != null && to != null) {
+                String fromTitle = (String) from.getOrDefault("title", "");
+                String toTitle = (String) to.getOrDefault("title", "");
+                if (!Objects.equals(fromTitle, toTitle)) {
+                    fields.add(new FieldDiff("title", fromTitle, toTitle));
+                }
+                String fromContent = (String) from.getOrDefault("content", "");
+                String toContent = (String) to.getOrDefault("content", "");
+                if (!Objects.equals(fromContent, toContent)) {
+                    fields.add(new FieldDiff("content", "length=" + fromContent.length(), "length=" + toContent.length()));
+                }
+            }
+            diff.setFieldDiffs(fields);
+            diff.setSummary(fields.size() + " field(s) changed");
+        } catch (Exception e) {
+            diff.setSummary("Version diff failed: " + e.getMessage());
+        }
+        return diff;
     }
 
     @Override
-    public boolean restore(Long companyId, Long knowledgeId, int fromVersion) {
+    public boolean rollback(Long companyId, Long knowledgeId, int targetVersion, String rollbackReason) {
         if (auxMapper == null) return false;
-        try { auxMapper.restoreVersion(companyId, knowledgeId, fromVersion); return true; }
-        catch (Exception e) { return false; }
+        try {
+            auxMapper.rollbackVersion(companyId, knowledgeId, targetVersion);
+            return true;
+        } catch (Exception e) {
+            logger.error("[KnowledgeVersion] rollback failed: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    @Override
+    public void addVersionTag(Long companyId, Long knowledgeId, int version, String tag) {
+        if (auxMapper == null) return;
+        try {
+            auxMapper.update("UPDATE lobster_knowledge_version SET tag='" + tag.replace("'", "''")
+                    + "' WHERE company_id=" + companyId + " AND knowledge_id=" + knowledgeId
+                    + " AND version=" + version);
+        } catch (Exception e) {
+            logger.warn("[KnowledgeVersion] addVersionTag failed: {}", e.getMessage());
+        }
+    }
+
+    private KnowledgeVersion mapToKnowledgeVersion(Map<String, Object> row) {
+        KnowledgeVersion kv = new KnowledgeVersion();
+        kv.setId(row.get("id") instanceof Number ? ((Number) row.get("id")).longValue() : null);
+        kv.setCompanyId(row.get("company_id") instanceof Number ? ((Number) row.get("company_id")).longValue() : null);
+        kv.setKnowledgeId(row.get("knowledge_id") instanceof Number ? ((Number) row.get("knowledge_id")).longValue() : null);
+        kv.setVersion(row.get("version") instanceof Number ? ((Number) row.get("version")).intValue() : 0);
+        kv.setTitle((String) row.get("title"));
+        kv.setContentSnapshot((String) row.get("content"));
+        kv.setTag((String) row.get("tag"));
+        Object changeLog = row.get("change_log");
+        if (changeLog != null) {
+            String log = changeLog.toString();
+            kv.setChangeReason(log);
+            kv.setChangeType(log.contains(":") ? log.substring(0, log.indexOf(":")) : log);
+        }
+        return kv;
     }
 }

+ 52 - 19
fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/DistributedLearningServiceImpl.java

@@ -23,41 +23,74 @@ public class DistributedLearningServiceImpl implements DistributedLearningServic
     private MultiModelRouter multiModelRouter;
 
     @Override
-    public void sharePattern(Long companyId, String industry, String scenario, String patternType, String content, double score) {
-        if (auxMapper == null) return;
+    public void contributePatterns(Long companyId, String industry, List<Map<String, Object>> patterns) {
+        if (auxMapper == null || patterns == null) return;
         try {
-            auxMapper.upsertDistributedPattern(companyId, industry, scenario, patternType, content, score);
+            for (Map<String, Object> pattern : patterns) {
+                String scenario = (String) pattern.getOrDefault("scenario", "general");
+                String patternType = (String) pattern.getOrDefault("patternType", "auto");
+                String content = (String) pattern.getOrDefault("content", JSON.toJSONString(pattern));
+                double score = pattern.get("score") instanceof Number ? ((Number) pattern.get("score")).doubleValue() : 0.5;
+                auxMapper.upsertDistributedPattern(companyId, industry, scenario, patternType, content, score);
+            }
         } catch (Exception e) {
-            logger.error("[DistributedLearning] sharePattern failed: {}", e.getMessage());
+            logger.error("[DistributedLearning] contributePatterns failed: {}", e.getMessage());
         }
     }
 
     @Override
-    public List<Map<String, Object>> getSharedPatterns(Long companyId, String industry, String scenario) {
-        if (auxMapper == null) return new ArrayList<>();
+    public List<IndustryBestPractice> getIndustryBestPractices(Long companyId, String industry, String scenario, int topK) {
+        List<IndustryBestPractice> result = new ArrayList<>();
+        if (auxMapper == null) return result;
         try {
-            return auxMapper.selectDistributedPatterns(companyId, industry, scenario);
-        } catch (Exception e) { return new ArrayList<>(); }
-    }
-
-    @Override
-    public Map<String, Object> getSharedPractice(Long practiceId) {
-        if (auxMapper == null) return null;
-        try { return auxMapper.selectDistributedPractice(practiceId); }
-        catch (Exception e) { return null; }
+            List<Map<String, Object>> rows = auxMapper.selectDistributedPatterns(null, industry, scenario);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    IndustryBestPractice practice = new IndustryBestPractice();
+                    practice.setId(row.get("id") instanceof Number ? ((Number) row.get("id")).longValue() : null);
+                    practice.setIndustry((String) row.getOrDefault("industry", industry));
+                    practice.setScenario((String) row.getOrDefault("scenario", scenario));
+                    practice.setContent((String) row.get("content"));
+                    practice.setPatternType((String) row.get("pattern_type"));
+                    practice.setEffectivenessScore(row.get("score") instanceof Number ? ((Number) row.get("score")).doubleValue() : 0.0);
+                    practice.setContributorCount(1);
+                    result.add(practice);
+                    if (result.size() >= topK) break;
+                }
+            }
+        } catch (Exception e) {
+            logger.error("[DistributedLearning] getIndustryBestPractices failed: {}", e.getMessage());
+        }
+        return result;
     }
 
     @Override
-    public void incrementPracticeUsage(Long practiceId) {
-        if (auxMapper == null) return;
-        try { auxMapper.incrementPracticeUsage(practiceId); } catch (Exception ignored) {}
+    public boolean applyBestPractice(Long companyId, Long practiceId) {
+        if (auxMapper == null) return false;
+        try {
+            Map<String, Object> practice = auxMapper.selectDistributedPractice(practiceId);
+            if (practice == null) return false;
+            auxMapper.incrementPracticeUsage(practiceId);
+            // 将实践内容写回本租户
+            String industry = (String) practice.getOrDefault("industry", "general");
+            String scenario = (String) practice.getOrDefault("scenario", "general");
+            String patternType = (String) practice.getOrDefault("pattern_type", "applied");
+            String content = (String) practice.get("content");
+            double score = practice.get("score") instanceof Number ? ((Number) practice.get("score")).doubleValue() : 0.6;
+            auxMapper.upsertDistributedPattern(companyId, industry, scenario, patternType, content, score);
+            return true;
+        } catch (Exception e) {
+            logger.error("[DistributedLearning] applyBestPractice failed: {}", e.getMessage());
+            return false;
+        }
     }
 
     @Override
-    public Map<String, Object> getStats() {
+    public Map<String, Object> getIndustryStats(String industry) {
         Map<String, Object> stats = new HashMap<>();
         if (auxMapper == null) return stats;
         try {
+            stats.put("industry", industry);
             stats.put("patternCount", auxMapper.countDistributedPatterns(null));
             stats.put("practiceCount", auxMapper.countDistributedPractices(null));
             stats.put("avgScore", auxMapper.avgDistributedScore(null));

+ 25 - 1
fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/TenantLearningEngineImpl.java

@@ -3,6 +3,7 @@ package com.fs.company.service.workflow.learning.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import com.fs.company.mapper.LobsterTenantLearningMapper;
 import com.fs.company.service.llm.MultiModelRouter;
 import com.fs.company.service.workflow.learning.*;
@@ -130,7 +131,7 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
                         "TenantLearning auto-apply");
                 }
             }
-            return ApplyResult.ok("学习结果已提交为补丁,等待审核应用");
+            return ApplyResult.success("学习结果已提交为补丁,等待审核应用", patterns != null ? patterns.size() : 0);
         } catch (Exception e) {
             return ApplyResult.fail("应用失败: " + e.getMessage());
         }
@@ -147,6 +148,29 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
         } catch (Exception e) { logger.debug("[TenantLearning] 学习周期异常: {}", e.getMessage()); }
     }
 
+    @Override
+    public Map<String, Object> getLearningMetrics(Long companyId) {
+        Map<String, Object> metrics = new HashMap<>();
+        if (learningMapper == null) {
+            metrics.put("status", "unavailable");
+            return metrics;
+        }
+        try {
+            int eventCount = learningMapper.countQualityEvents(companyId, null);
+            List<Map<String, Object>> patterns = learningMapper.selectPatterns(companyId);
+            metrics.put("eventCount", eventCount);
+            metrics.put("patternCount", patterns != null ? patterns.size() : 0);
+            metrics.put("status", "active");
+            metrics.put("minEventsForLearning", MIN_EVENTS_FOR_LEARNING);
+        } catch (Exception e) {
+            logger.error("[TenantLearning] getLearningMetrics failed", e);
+            metrics.put("status", "error");
+            metrics.put("error", e.getMessage());
+        }
+        return metrics;
+    }
+
+    @Override
     public void ingestCorpusKnowledge(Long companyId, SalesCorpusAnalyzer.AnalysisReport report) {
         if (learningMapper == null || report == null) return;
         try {

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/workflow/personalization/impl/PersonalizationEngineImpl.java

@@ -2,6 +2,8 @@ package com.fs.company.service.workflow.personalization.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.fs.company.service.workflow.personalization.PersonalizationEngine;
+import com.fs.company.mapper.LobsterAuxiliaryMapper;
+import com.fs.company.mapper.LobsterSegmentMapper;
 import com.fs.company.mapper.LobsterUserPreferenceMapper;
 import com.fs.company.service.workflow.personalization.*;
 import org.slf4j.Logger;

+ 67 - 0
fs-service/src/main/java/com/fs/company/service/workflow/queue/DeadLetterQueue.java

@@ -1,5 +1,7 @@
 package com.fs.company.service.workflow.queue;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.company.mapper.LobsterAuxiliaryMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -7,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.function.Predicate;
 
 @Service
 public class DeadLetterQueue {
@@ -28,4 +31,68 @@ public class DeadLetterQueue {
 
     public void retry(Long id) { if (auxMapper != null) auxMapper.retryDeadLetter(id); }
     public void remove(Long id) { if (auxMapper != null) auxMapper.deleteDeadLetter(id); }
+
+    public List<DeadMessage> getDeadLetterList() {
+        List<DeadMessage> result = new ArrayList<>();
+        if (auxMapper == null) return result;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDeadLetters(null, 100);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    DeadMessage dm = new DeadMessage();
+                    dm.id = row.get("id") instanceof Number ? ((Number) row.get("id")).longValue() : null;
+                    dm.companyId = row.get("company_id") instanceof Number ? ((Number) row.get("company_id")).longValue() : null;
+                    dm.queueName = (String) row.get("queue_name");
+                    String payload = (String) row.get("payload");
+                    if (payload != null) {
+                        try {
+                            JSONObject p = JSON.parseObject(payload);
+                            dm.instanceId = p.getLong("instanceId");
+                            dm.channelType = p.getString("channelType");
+                        } catch (Exception ignored) {}
+                    }
+                    dm.error = (String) row.get("error");
+                    result.add(dm);
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DeadLetter] getDeadLetterList failed: {}", e.getMessage());
+        }
+        return result;
+    }
+
+    public int getPendingCount() {
+        return getDeadLetterList().size();
+    }
+
+    public int getDeadCount() {
+        return getDeadLetterList().size();
+    }
+
+    public int retryAllDead(Predicate<DeadMessage> filter) {
+        int success = 0;
+        List<DeadMessage> list = getDeadLetterList();
+        for (DeadMessage dm : list) {
+            try {
+                if (filter == null || filter.test(dm)) {
+                    if (dm.id != null) {
+                        retry(dm.id);
+                    }
+                    success++;
+                }
+            } catch (Exception e) {
+                logger.warn("[DeadLetter] retryAllDead failed for id={}: {}", dm.id, e.getMessage());
+            }
+        }
+        return success;
+    }
+
+    public static class DeadMessage {
+        public Long id;
+        public Long companyId;
+        public Long instanceId;
+        public String queueName;
+        public String channelType;
+        public String error;
+    }
 }

+ 2 - 2
fs-service/src/main/java/com/fs/company/service/workflow/scheduler/WorkflowTriggerScheduler.java

@@ -72,7 +72,7 @@ public class WorkflowTriggerScheduler {
         try {
             CompanyWorkflowLobster q = new CompanyWorkflowLobster();
             q.setStatus(1); // 仅启用
-            List<CompanyWorkflowLobster> list = workflowMapper.selectCompanyWorkflowLobsterList(q);
+            List<CompanyWorkflowLobster> list = workflowMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q));
             if (list == null || list.isEmpty()) return;
 
             LocalDateTime now = LocalDateTime.now();
@@ -99,7 +99,7 @@ public class WorkflowTriggerScheduler {
             CompanyWorkflowLobster q = new CompanyWorkflowLobster();
             q.setCompanyId(companyId);
             q.setStatus(1);
-            List<CompanyWorkflowLobster> list = workflowMapper.selectCompanyWorkflowLobsterList(q);
+            List<CompanyWorkflowLobster> list = workflowMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<>(q));
             if (list == null) return;
             for (CompanyWorkflowLobster wf : list) {
                 String evtCfg = parseEventTypeFromCanvas(wf.getCanvasData());

+ 86 - 85
fs-service/src/main/java/com/fs/company/service/workflow/semantic/impl/SemanticAnalyzerImpl.java

@@ -18,106 +18,107 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
- * 语义分析器实�?
+ * 语义分析器实
  *
- * 核心能力�?
- * 1. 意图识别:AI语义分析(主�?+ 关键词匹配(降级�?
- * 2. 情感分析:AI情感评分(主�?+ 增强型词典评分(降级�?
- * 3. 关键词提取:AI提取(主�?+ 多策略提取(降级�?
+ * 核心能力
+ * 1. 意图识别:AI语义分析(主) + 关键词匹配(降级)
+ * 2. 情感分析:AI情感评分(主) + 增强型词典评分(降级)
+ * 3. 关键词提取:AI提取(主) + 多策略提取(降级)
  * 4. 对话摘要:AI生成摘要
  *
- * 降级策略�?
+ * 降级策略
  * - AI分析失败时自动降级为规则匹配
  * - 规则匹配使用增强型词典,支持否定词、程度词、行业词
- * - 降级结果置信度自动降低,确保下游可区�?
+ * - 降级结果置信度自动降低,确保下游可区
  *
- * 性能优化�?
+ * 性能优化
  * - 租户自定义词典缓存在内存中,5分钟刷新
- * - AI分析使用轻量模型(doubao-lite),降低延迟和成�?
+ * - AI分析使用轻量模型(doubao-lite),降低延迟和成
  */
-@Slf4j
 @Service
 public class SemanticAnalyzerImpl implements SemanticAnalyzer {
 
+    private static final Logger log = LoggerFactory.getLogger(SemanticAnalyzerImpl.class);
+
     @Autowired
     private MultiModelRouter multiModelRouter;
 
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
-    /** 基础意图关键词映射(降级使用�?*/
+    /** 基础意图关键词映射(降级使用*/
     private static final Map<String, String[]> INTENT_KEYWORDS = new LinkedHashMap<>();
     static {
-        INTENT_KEYWORDS.put("purchase", new String[]{"�?, "�?, "下单", "订购", "价格", "多少�?, "费用", "付费", "套餐", "优惠", "折扣", "活动"});
-        INTENT_KEYWORDS.put("complaint", new String[]{"投诉", "不满", "�?, "�?, "退�?, "退�?, "�?, "举报", "律师", "消费者协�?});
-        INTENT_KEYWORDS.put("inquiry", new String[]{"咨询", "了解", "�?, "怎么", "如何", "什�?, "为什�?, "介绍", "功能", "详情"});
-        INTENT_KEYWORDS.put("schedule", new String[]{"时间", "什么时�?, "几点", "预约", "安排", "见面", "档期"});
-        INTENT_KEYWORDS.put("positive", new String[]{"�?, "不错", "感兴�?, "可以", "�?, "喜欢", "满意", "感谢", "谢谢", "好的", "同意", "没问�?});
-        INTENT_KEYWORDS.put("negative", new String[]{"不要", "不需�?, "没兴�?, "算了", "不用�?, "拒绝", "不考虑", "暂时不需�?});
-        INTENT_KEYWORDS.put("churn", new String[]{"取消", "退�?, "不用�?, "换别�?, "考虑其他", "太贵�?, "不划�?, "别联系了", "拉黑"});
+        INTENT_KEYWORDS.put("purchase", new String[]{"买", "购买", "下单", "订购", "价格", "多少钱", "费用", "付费", "套餐", "优惠", "折扣", "活动"});
+        INTENT_KEYWORDS.put("complaint", new String[]{"投诉", "不满", "差评", "垃圾", "退款", "退货", "维权", "举报", "律师", "消费者协会"});
+        INTENT_KEYWORDS.put("inquiry", new String[]{"咨询", "了解", "询问", "怎么", "如何", "什么", "为什么", "介绍", "功能", "详情"});
+        INTENT_KEYWORDS.put("schedule", new String[]{"时间", "什么时候", "几点", "预约", "安排", "见面", "档期"});
+        INTENT_KEYWORDS.put("positive", new String[]{"好", "不错", "感兴趣", "可以", "行", "喜欢", "满意", "感谢", "谢谢", "好的", "同意", "没问题"});
+        INTENT_KEYWORDS.put("negative", new String[]{"不要", "不需要", "没兴趣", "算了", "不用了", "拒绝", "不考虑", "暂时不需要"});
+        INTENT_KEYWORDS.put("churn", new String[]{"取消", "退订", "不用了", "换别的", "考虑其他", "太贵了", "不划算", "别联系了", "拉黑"});
     }
 
-    /** 增强型情感词�?- 积极词(带权重) */
+    /** 增强型情感词- 积极词(带权重) */
     private static final Map<String, Double> POSITIVE_WORDS = new LinkedHashMap<>();
     static {
-        POSITIVE_WORDS.put("�?, 0.15);
+        POSITIVE_WORDS.put("好", 0.15);
         POSITIVE_WORDS.put("不错", 0.2);
         POSITIVE_WORDS.put("很好", 0.3);
-        POSITIVE_WORDS.put("非常�?, 0.4);
+        POSITIVE_WORDS.put("非常好", 0.4);
         POSITIVE_WORDS.put("满意", 0.3);
         POSITIVE_WORDS.put("感谢", 0.2);
         POSITIVE_WORDS.put("谢谢", 0.2);
         POSITIVE_WORDS.put("喜欢", 0.25);
-        POSITIVE_WORDS.put("感兴�?, 0.2);
+        POSITIVE_WORDS.put("感兴趣", 0.2);
         POSITIVE_WORDS.put("可以", 0.1);
-        POSITIVE_WORDS.put("�?, 0.1);
+        POSITIVE_WORDS.put("行", 0.1);
         POSITIVE_WORDS.put("同意", 0.2);
-        POSITIVE_WORDS.put("没问�?, 0.15);
-        POSITIVE_WORDS.put("太好�?, 0.35);
-        POSITIVE_WORDS.put("�?, 0.3);
+        POSITIVE_WORDS.put("没问题", 0.15);
+        POSITIVE_WORDS.put("太好了", 0.35);
+        POSITIVE_WORDS.put("很棒", 0.3);
     }
 
-    /** 增强型情感词�?- 消极词(带权重) */
+    /** 增强型情感词- 消极词(带权重) */
     private static final Map<String, Double> NEGATIVE_WORDS = new LinkedHashMap<>();
     static {
         NEGATIVE_WORDS.put("不要", 0.15);
-        NEGATIVE_WORDS.put("不需�?, 0.2);
-        NEGATIVE_WORDS.put("没兴�?, 0.25);
-        NEGATIVE_WORDS.put("�?, 0.2);
-        NEGATIVE_WORDS.put("�?, 0.35);
-        NEGATIVE_WORDS.put("�?, 0.4);
+        NEGATIVE_WORDS.put("不需要", 0.2);
+        NEGATIVE_WORDS.put("没兴趣", 0.25);
+        NEGATIVE_WORDS.put("差", 0.2);
+        NEGATIVE_WORDS.put("烂", 0.35);
+        NEGATIVE_WORDS.put("糟糕", 0.4);
         NEGATIVE_WORDS.put("投诉", 0.4);
-        NEGATIVE_WORDS.put("退�?, 0.3);
+        NEGATIVE_WORDS.put("退款", 0.3);
         NEGATIVE_WORDS.put("不满", 0.3);
         NEGATIVE_WORDS.put("拒绝", 0.25);
         NEGATIVE_WORDS.put("算了", 0.15);
-        NEGATIVE_WORDS.put("太差�?, 0.45);
+        NEGATIVE_WORDS.put("太差了", 0.45);
         NEGATIVE_WORDS.put("垃圾", 0.4);
         NEGATIVE_WORDS.put("什么破", 0.4);
     }
 
-    /** 否定词列�?- 遇到否定词时反转后续情感词的极�?*/
-    private static final String[] NEGATION_WORDS = {"�?, "�?, "�?, "�?, "�?, "�?, "不太", "不是�?};
+    /** 否定词列表 - 遇到否定词时反转后续情感词的极性 */
+    private static final String[] NEGATION_WORDS = {"不", "没", "无", "非", "未", "否", "不太", "不是"};
 
-    /** 程度词列�?- 遇到程度词时增强后续情感词的权重 */
+    /** 程度词列- 遇到程度词时增强后续情感词的权重 */
     private static final Map<String, Double> DEGREE_WORDS = new LinkedHashMap<>();
     static {
         DEGREE_WORDS.put("非常", 1.5);
         DEGREE_WORDS.put("特别", 1.5);
         DEGREE_WORDS.put("极其", 1.8);
-        DEGREE_WORDS.put("�?, 1.6);
-        DEGREE_WORDS.put("�?, 1.3);
+        DEGREE_WORDS.put("十分", 1.6);
+        DEGREE_WORDS.put("很", 1.3);
         DEGREE_WORDS.put("相当", 1.4);
         DEGREE_WORDS.put("比较", 1.1);
         DEGREE_WORDS.put("稍微", 0.6);
     }
 
-    /** 租户自定义词典缓�?*/
+    /** 租户自定义词典缓*/
     private final Map<Long, Map<String, String[]>> tenantKeywordCache = new java.util.concurrent.ConcurrentHashMap<>();
     private final Map<Long, Long> cacheRefreshTime = new java.util.concurrent.ConcurrentHashMap<>();
     private static final long CACHE_TTL_MS = 5 * 60 * 1000L;
 
-    /** 中文分词正则:提取中文词�?*/
+    /** 中文分词正则:提取中文词*/
     private static final Pattern CHINESE_WORD_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]{2,}");
 
     @Override
@@ -129,26 +130,26 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
         /*
          * 主策略:AI语义分析
          * 使用轻量模型进行意图识别,返回结构化JSON结果
-         * AI分析能理解上下文和隐含意图,准确率远高于关键词匹�?
+         * AI分析能理解上下文和隐含意图,准确率远高于关键词匹
          *
-         * Prompt设计要点�?
-         * - 明确指定意图类型枚举,避免AI返回非标准意�?
-         * - 要求返回结构化JSON,便于程序解�?
-         * - 包含上下文信息,提升意图识别准确�?
-         * - 情感分析要求返回-1�?的连续值,而非简单正/负标�?
+         * Prompt设计要点
+         * - 明确指定意图类型枚举,避免AI返回非标准意
+         * - 要求返回结构化JSON,便于程序解
+         * - 包含上下文信息,提升意图识别准确
+         * - 情感分析要求返回-1到1的连续值,而非简单正/负标签
          */
         try {
-            String contextStr = context != null ? JSON.toJSONString(context) : "�?;
+            String contextStr = context != null ? JSON.toJSONString(context) : "{}";
             String prompt = "你是一个意图分析专家,只返回JSON格式结果,不要其他内容。\n\n请分析以下客户消息的意图和情感,返回JSON格式:\n" +
-                    "【消息内容�? + message + "\n" +
-                    "【上下文�? + contextStr + "\n\n" +
+                    "【消息内容】" + message + "\n" +
+                    "【上下文】" + contextStr + "\n\n" +
                     "分析要求:\n" +
                     "1. intent: 意图类型,必须为以下之一:purchase(购买意向),inquiry(咨询了解),complaint(投诉不满)," +
                     "positive(积极回应),negative(拒绝消极),schedule(预约安排),churn(流失风险),other(其他)\n" +
-                    "2. confidence: 置信�?0-1),你对意图判断的确定程度\n" +
-                    "3. sentiment: 情感�?-1�?)�?1=极度消极�?=中性,1=极度积极\n" +
+                    "2. confidence: 置信度(0-1),你对意图判断的确定程度\n" +
+                    "3. sentiment: 情感值(-1到1),-1=极度消极,0=中性,1=极度积极\n" +
                     "4. keywords: 3-5个关键信息词,反映客户关注的核心内容\n\n" +
-                    "返回格式:{\"intent\":\"意图\",\"confidence\":0.9,\"sentiment\":0.5,\"keywords\":[\"关键�?\",\"关键�?\"]}";
+                    "返回格式:{\"intent\":\"意图\",\"confidence\":0.9,\"sentiment\":0.5,\"keywords\":[\"关键词1\",\"关键词2\"]}";
             String aiResponse = multiModelRouter.generateResponse(prompt, "doubao-lite",
                     "semantic_analyzer");
             SemanticResult aiResult = parseSemanticResult(aiResponse);
@@ -157,13 +158,13 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
                 return aiResult;
             }
         } catch (Exception e) {
-            log.warn("[SemanticAnalyzer] AI意图分析失败,降级为关键词匹�? {}", e.getMessage());
+            log.warn("[SemanticAnalyzer] AI意图分析失败,降级为关键词匹 {}", e.getMessage());
         }
 
         /*
-         * 降级策略:增强型关键词匹�?
-         * 使用带权重的情感词典 + 否定�?程度词处�?
-         * 置信度自动降低,让下游知道这是降级结�?
+         * 降级策略:增强型关键词匹
+         * 使用带权重的情感词典 + 否定词/程度词处理
+         * 置信度自动降低,让下游知道这是降级结
          */
         String detectedIntent = detectIntentByKeywords(message);
         Double sentiment = calculateSentimentEnhanced(message);
@@ -202,14 +203,14 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
     }
 
     /**
-     * 增强型情感分�?
-     * 使用带权重的情感词典 + 否定�?程度词处�?
+     * 增强型情感分
+     * 使用带权重的情感词典 + 否定词/程度词处理
      * 替代旧版的简单正负词计数
      *
-     * 处理逻辑�?
-     * 1. 遍历消息中的每个�?
-     * 2. 如果是程度词,记录增强系�?
-     * 3. 如果是否定词,标记反�?
+     * 处理逻辑
+     * 1. 遍历消息中的每个
+     * 2. 如果是程度词,记录增强系
+     * 3. 如果是否定词,标记反
      * 4. 如果是情感词,根据程度词和否定词调整权重
      * 5. 最终归一化到[-1, 1]区间
      */
@@ -219,7 +220,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
     }
 
     /**
-     * 增强型情感分析实�?
+     * 增强型情感分析实
      */
     private Double calculateSentimentEnhanced(String message) {
         if (message == null || message.trim().isEmpty()) {
@@ -232,7 +233,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
 
         /*
          * 第一遍:使用带权重的情感词典
-         * 处理否定词和程度词对情感的影�?
+         * 处理否定词和程度词对情感的影
          */
         for (Map.Entry<String, Double> entry : POSITIVE_WORDS.entrySet()) {
             int idx = message.indexOf(entry.getKey());
@@ -269,7 +270,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
         }
 
         /*
-         * 第二遍:检查租户自定义情感�?
+         * 第二遍:检查租户自定义情感
          * 从数据库加载租户配置的行业特定情感词
          */
         Map<String, String[]> tenantWords = getTenantCustomKeywords(null);
@@ -289,7 +290,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
 
     /**
      * 检查情感词前面是否有否定词
-     * 例如�?不好" �?"�?前面�?�?,情感反�?
+     * 例如:"不好" 中 "好" 前面有 "不",情感反转
      */
     private boolean isPrecededByNegation(String message, int wordIndex) {
         for (String negation : NEGATION_WORDS) {
@@ -305,8 +306,8 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
     }
 
     /**
-     * 检查情感词前面是否有程度词,返回增强系�?
-     * 例如�?非常�? �?"�?前面�?非常",权重�?.5
+     * 检查情感词前面是否有程度词,返回增强系
+     * 例如:"非常好" 中 "好" 前面是"非常",权重×1.5
      */
     private double isPrecededByDegree(String message, int wordIndex) {
         for (Map.Entry<String, Double> entry : DEGREE_WORDS.entrySet()) {
@@ -323,7 +324,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
 
     /**
      * 增强型关键词提取
-     * 使用多策略提取:意图关键�?+ 高频中文词组 + 租户自定义词
+     * 使用多策略提取:意图关键+ 高频中文词组 + 租户自定义词
      */
     @Override
     public List<String> extractKeywords(String message) {
@@ -354,15 +355,15 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
         }
 
         /*
-         * 策略2:中文词组提�?
-         * 使用正则提取2字及以上的中文词�?
-         * 过滤掉常见的停用�?
+         * 策略2:中文词组提
+         * 使用正则提取2字及以上的中文词
+         * 过滤掉常见的停用
          */
         Matcher matcher = CHINESE_WORD_PATTERN.matcher(message);
         Set<String> stopWords = new HashSet<>(Arrays.asList(
-                "的话", "的话�?, "那么", "然后", "所�?, "因为", "但是", "不过",
-                "如果", "虽然", "而且", "或�?, "以及", "这个", "那个", "什�?,
-                "怎么", "为什�?, "哪里", "哪个", "多少", "几个", "这样", "那样"));
+                "的话", "那么", "然后", "所以", "因为", "但是", "不过",
+                "如果", "虽然", "而且", "或者", "以及", "这个", "那个", "什么",
+                "怎么", "为什么", "哪里", "哪个", "多少", "几个", "这样", "那样"));
         while (matcher.find()) {
             String word = matcher.group();
             if (!stopWords.contains(word) && word.length() >= 2 && word.length() <= 6) {
@@ -371,7 +372,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
         }
 
         /*
-         * 策略3:租户自定义关键�?
+         * 策略3:租户自定义关键
          * 从数据库加载租户配置的行业特定关键词
          */
         Map<String, String[]> tenantWords = getTenantCustomKeywords(null);
@@ -392,8 +393,8 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
         if (keywords.size() < 3) {
             try {
                 String prompt = "你是关键词提取专家,只返回JSON数组。\n\n从以下消息中提取3-5个关键词,以JSON数组格式返回:\n" +
-                        "消息�? + message + "\n" +
-                        "格式:[\"关键�?\",\"关键�?\",\"关键�?\"]";
+                        "消息:" + message + "\n" +
+                        "格式:[\"关键词1\",\"关键词2\",\"关键词3\"]";
                 String aiResponse = multiModelRouter.generateResponse(prompt, "doubao-lite",
                         "semantic_analyzer");
                 int start = aiResponse.indexOf("[");
@@ -410,7 +411,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
                     }
                 }
             } catch (Exception e) {
-                log.debug("[SemanticAnalyzer] AI关键词提取失�? {}", e.getMessage());
+                log.debug("[SemanticAnalyzer] AI关键词提取失 {}", e.getMessage());
             }
         }
 
@@ -418,7 +419,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
     }
 
     /**
-     * 基于关键词的意图检测(降级方案�?
+     * 基于关键词的意图检测(降级方案
      * 使用加权匹配,匹配词数越多置信度越高
      */
     private String detectIntentByKeywords(String message) {
@@ -449,7 +450,7 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
     /**
      * 获取租户自定义关键词
      * 从数据库加载租户配置的行业特定关键词
-     * 缓存5分钟,避免频繁查�?
+     * 缓存5分钟,避免频繁查
      */
     private Map<String, String[]> getTenantCustomKeywords(Long companyId) {
         if (companyId == null) {
@@ -502,13 +503,13 @@ public class SemanticAnalyzerImpl implements SemanticAnalyzer {
                             "create_time DATETIME DEFAULT NOW(), " +
                             "update_time DATETIME DEFAULT NOW(), " +
                             "INDEX idx_company_category (company_id, category)" +
-                            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户自定义关键词配置�?");
-            log.info("[SemanticAnalyzer] 自动创建了lobster_tenant_keywords�?);
+                            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户自定义关键词配置");
+            log.info("[SemanticAnalyzer] 自动创建了lobster_tenant_keywords表");
         }
     }
 
     /**
-     * 解析AI返回的语义分析结�?
+     * 解析AI返回的语义分析结
      * 从AI回复中提取JSON部分并转换为SemanticResult
      */
     private SemanticResult parseSemanticResult(String aiResponse) {

+ 1 - 1
fs-task/src/main/java/com/fs/task/TaskPackages.java

@@ -20,7 +20,7 @@ public final class TaskPackages {
     /**
      * The packages that contain the actual @Component task entry points and supporting services.
      * These are scanned by TaskRegistryService to build the list of classes/methods available
-     * for configuration as sys_job.invoke_target (e.g. "qWTask.��ʱ��ȡȺ��").
+     * for configuration as sys_job.invoke_target (e.g. "qWTask.定时拉取群聊").
      *
      * Current layout after reorganization:
      *   com.fs.task.jobs          -- top-level entry schedulers (QwTask, CourseWatchLogScheduler, ...)