云联一号 3 дней назад
Родитель
Сommit
86187e9177
100 измененных файлов с 5392 добавлено и 1036 удалено
  1. 2 0
      fs-admin-saas/src/main/java/com/fs/FsSaasAdminApplication.java
  2. 912 0
      fs-admin-saas/src/main/java/com/fs/lobster/controller/LobsterAdminController.java
  3. 43 0
      fs-admin-saas/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql
  4. 0 637
      fs-admin/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java
  5. 43 0
      fs-agent/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql
  6. 131 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java
  7. 10 48
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterBillingController.java
  8. 13 41
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEventAuditController.java
  9. 108 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterModelRouteController.java
  10. 29 66
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPromptController.java
  11. 5 27
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterSalesCorpusController.java
  12. 21 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java
  13. 176 0
      fs-service/src/main/java/com/fs/company/controller/LobsterAdminController.java
  14. 71 0
      fs-service/src/main/java/com/fs/company/controller/PayCallbackController.java
  15. 6 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterNode.java
  16. 29 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChannelPluginConfig.java
  17. 22 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChatMsg.java
  18. 28 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChatSession.java
  19. 20 0
      fs-service/src/main/java/com/fs/company/domain/LobsterComplianceAudit.java
  20. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterConsumeRecord.java
  21. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterDedupConfig.java
  22. 38 0
      fs-service/src/main/java/com/fs/company/domain/LobsterEventAudit.java
  23. 40 0
      fs-service/src/main/java/com/fs/company/domain/LobsterProfileConfig.java
  24. 36 0
      fs-service/src/main/java/com/fs/company/domain/LobsterSalesCorpus.java
  25. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterSummaryConfig.java
  26. 40 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTenantBalance.java
  27. 33 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTokenConsumption.java
  28. 14 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java
  29. 29 0
      fs-service/src/main/java/com/fs/company/mapper/CustomerFactMapper.java
  30. 25 0
      fs-service/src/main/java/com/fs/company/mapper/CustomerHabitMapper.java
  31. 36 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterApiRegistryMapper.java
  32. 203 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java
  33. 27 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterBillingMapper.java
  34. 44 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChannelPluginConfigMapper.java
  35. 35 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChannelRegistryMapper.java
  36. 18 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatMsgMapper.java
  37. 28 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatRecordMapper.java
  38. 36 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatSessionMapper.java
  39. 14 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceAuditMapper.java
  40. 2 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceRuleMapper.java
  41. 25 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterConversationSummaryMapper.java
  42. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterDedupConfigMapper.java
  43. 24 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterDialogueStateMapper.java
  44. 28 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterEventAuditMapper.java
  45. 92 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java
  46. 57 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterFeedbackMapper.java
  47. 15 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterHandoffEventMapper.java
  48. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterKnowledgeUsageLogMapper.java
  49. 35 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterLearningCorpusMapper.java
  50. 65 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterMultiTurnDialogueMapper.java
  51. 3 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterNodeExecutionLogMapper.java
  52. 54 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterPendingKnowledgeMapper.java
  53. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterProfileConfigMapper.java
  54. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSalesCorpusMapper.java
  55. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSegmentMapper.java
  56. 26 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSensitiveWordMapper.java
  57. 13 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSmartApiMapper.java
  58. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSummaryConfigMapper.java
  59. 21 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSystemPromptMapper.java
  60. 93 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterTenantLearningMapper.java
  61. 33 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterTokenConsumptionMapper.java
  62. 37 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterToolCallMapper.java
  63. 32 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterUserPreferenceMapper.java
  64. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterUserProfileMapper.java
  65. 3 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterWorkflowInstanceMapper.java
  66. 40 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterWorkflowVariableMapper.java
  67. 34 0
      fs-service/src/main/java/com/fs/company/mapper/ProfileEnrichmentMapper.java
  68. 17 0
      fs-service/src/main/java/com/fs/company/mapper/WhatsAppContactMapper.java
  69. 96 8
      fs-service/src/main/java/com/fs/company/service/ai/AiSceneDispatcher.java
  70. 46 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java
  71. 7 0
      fs-service/src/main/java/com/fs/company/service/llm/MultiModelRouter.java
  72. 155 37
      fs-service/src/main/java/com/fs/company/service/llm/impl/ModelRouterImpl.java
  73. 59 8
      fs-service/src/main/java/com/fs/company/service/llm/impl/MultiModelRouterImpl.java
  74. 103 0
      fs-service/src/main/java/com/fs/company/service/workflow/DynamicNodeImplService.java
  75. 32 0
      fs-service/src/main/java/com/fs/company/service/workflow/ILobsterBillingService.java
  76. 19 0
      fs-service/src/main/java/com/fs/company/service/workflow/ILobsterEventAuditService.java
  77. 18 0
      fs-service/src/main/java/com/fs/company/service/workflow/ILobsterSalesCorpusService.java
  78. 200 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterE2eTestService.java
  79. 9 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterEvolutionEngine.java
  80. 31 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterTestScenarioService.java
  81. 9 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterWorkflowExecutor.java
  82. 8 2
      fs-service/src/main/java/com/fs/company/service/workflow/QualityScoringService.java
  83. 6 0
      fs-service/src/main/java/com/fs/company/service/workflow/SensitiveWordService.java
  84. 17 3
      fs-service/src/main/java/com/fs/company/service/workflow/SummaryGenerator.java
  85. 98 0
      fs-service/src/main/java/com/fs/company/service/workflow/api/ApiCallExecutor.java
  86. 23 113
      fs-service/src/main/java/com/fs/company/service/workflow/api/ApiRegistryService.java
  87. 47 0
      fs-service/src/main/java/com/fs/company/service/workflow/api/ScriptCache.java
  88. 144 0
      fs-service/src/main/java/com/fs/company/service/workflow/api/SmartApiCallNodeExecutor.java
  89. 206 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/ChannelPluginService.java
  90. 19 42
      fs-service/src/main/java/com/fs/company/service/workflow/channel/ChannelTypeRegistry.java
  91. 62 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/AppImMessageChannel.java
  92. 66 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinDmMessageChannel.java
  93. 63 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinEcMessageChannel.java
  94. 66 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/JdMessageChannel.java
  95. 62 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/KuaishouDmMessageChannel.java
  96. 62 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/LineMessageChannel.java
  97. 6 1
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/QwMessageChannel.java
  98. 62 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TelegramMessageChannel.java
  99. 72 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TmallMessageChannel.java
  100. 98 0
      fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/WhatsAppMessageChannel.java

+ 2 - 0
fs-admin-saas/src/main/java/com/fs/FsSaasAdminApplication.java

@@ -7,6 +7,7 @@ import org.redisson.spring.starter.RedissonAutoConfiguration;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.FilterType;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.Transactional;
 
 /**
@@ -15,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, RedissonAutoConfiguration.class})
 @Transactional
 @EnableAsync
+@EnableScheduling
 public class FsSaasAdminApplication {
     public static void main(String[] args) {
         SpringApplication.run(FsSaasAdminApplication.class, args);

+ 912 - 0
fs-admin-saas/src/main/java/com/fs/lobster/controller/LobsterAdminController.java

@@ -0,0 +1,912 @@
+package com.fs.lobster.controller;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.LobsterModelConfig;
+import com.fs.company.service.workflow.ILobsterBillingService;
+import com.fs.company.service.workflow.ILobsterEventAuditService;
+import com.fs.company.service.workflow.ILobsterSalesCorpusService;
+import com.fs.company.service.workflow.LobsterModelConfigService;
+import com.fs.framework.web.service.TokenService;
+import com.fs.proxy.domain.AiChatQualityRecord;
+import com.fs.proxy.service.AiChatQualityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 龙虾引擎管理端Controller(fs-admin-saas,替代原 AdminLobsterBridgeController)
+ * 全部使用 MyBatis Service,无 JdbcTemplate,无桥接镜像表
+ */
+@RestController
+public class LobsterAdminController extends BaseController {
+
+    @Autowired
+    private ILobsterSalesCorpusService salesCorpusService;
+
+    @Autowired
+    private ILobsterEventAuditService eventAuditService;
+
+    @Autowired
+    private ILobsterBillingService billingService;
+
+    @Autowired(required = false)
+    private LobsterModelConfigService modelConfigService;
+
+    @Autowired(required = false)
+    private AiChatQualityService aiChatQualityService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.config.LobsterCompanyConfigService companyConfigService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.LobsterE2eTestService e2eTestService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.LobsterTestScenarioService testScenarioService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.DynamicNodeImplService dynamicNodeImplService;
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.LobsterWorkflowExecutor workflowExecutor;
+
+    @Autowired(required = false)
+    private org.springframework.jdbc.core.JdbcTemplate jdbcTemplate;
+
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterChatSessionMapper chatSessionMapper;
+
+    @Autowired(required = false)
+    private com.fs.company.mapper.LobsterChatMsgMapper chatMsgMapper;
+
+    @Autowired
+    private TokenService tokenService;
+
+    // ======== 销冠语料 ========
+    @GetMapping({"/workflow/lobster/sales-corpus", "/workflow/lobster/sales-corpus/list",
+                 "/workflow/lobster/corpus", "/workflow/lobster/corpus/list"})
+    public AjaxResult lobsterCorpus(@RequestParam(defaultValue = "1") int page,
+                                     @RequestParam(defaultValue = "10") int size,
+                                     @RequestParam(required = false) String scenario,
+                                     @RequestParam(required = false) String status,
+                                     @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(salesCorpusService.listCorpus(page, size, companyId, scenario, status));
+    }
+
+    @GetMapping("/workflow/lobster/sales-corpus/scenarios")
+    public AjaxResult lobsterCorpusScenarios() {
+        return AjaxResult.success(salesCorpusService.getScenarios());
+    }
+
+    // ======== 节点审核(管理端聚合视图) ========
+    @GetMapping({"/workflow/lobster/event-audit", "/workflow/lobster/event-audit/list",
+                 "/workflow/lobster/eventAudit", "/workflow/lobster/eventAudit/list"})
+    public AjaxResult lobsterEventAuditList(@RequestParam(defaultValue = "pending") String status,
+                                             @RequestParam(defaultValue = "1") int page,
+                                             @RequestParam(defaultValue = "10") int size,
+                                             @RequestParam(required = false) Long companyId) {
+        return AjaxResult.success(eventAuditService.listAudits(status, page, size, companyId));
+    }
+
+    @GetMapping({"/workflow/lobster/event-audit/{id}", "/workflow/lobster/eventAudit/{id}"})
+    public AjaxResult lobsterEventAuditDetail(@PathVariable Long id) {
+        return AjaxResult.success(eventAuditService.getById(id));
+    }
+
+    @PostMapping({"/workflow/lobster/event-audit/approve/{id}", "/workflow/lobster/eventAudit/approve/{id}"})
+    public AjaxResult lobsterEventAuditApprove(@PathVariable Long id) {
+        LoginUser loginUser = (LoginUser) tokenService.getLoginUser(ServletUtils.getRequest());
+        eventAuditService.approve(id, loginUser.getUsername());
+        return AjaxResult.success("审批通过");
+    }
+
+    @PostMapping({"/workflow/lobster/event-audit/reject/{id}", "/workflow/lobster/eventAudit/reject/{id}"})
+    public AjaxResult lobsterEventAuditReject(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
+        LoginUser loginUser = (LoginUser) tokenService.getLoginUser(ServletUtils.getRequest());
+        String comment = body != null ? (String) body.getOrDefault("comment", "人工驳回") : "人工驳回";
+        eventAuditService.reject(id, loginUser.getUsername(), comment);
+        return AjaxResult.success("已驳回");
+    }
+
+    // ======== Token计费(管理端聚合视图) ========
+    @GetMapping({"/workflow/lobster/billing", "/workflow/lobster/billing/list"})
+    public AjaxResult lobsterBillingList(@RequestParam(defaultValue = "1") int page,
+                                          @RequestParam(defaultValue = "10") int size,
+                                          @RequestParam(required = false) Long tenantId) {
+        if (tenantId != null) {
+            return AjaxResult.success(billingService.listConsumeRecords(page, size, tenantId));
+        }
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster/billing/records")
+    public AjaxResult lobsterBillingRecords(@RequestParam(defaultValue = "1") int page,
+                                             @RequestParam(defaultValue = "10") int size,
+                                             @RequestParam(required = false) Long tenantId) {
+        if (tenantId != null) {
+            return AjaxResult.success(billingService.listConsumeRecords(page, size, tenantId));
+        }
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @GetMapping("/workflow/lobster/billing/stats")
+    public AjaxResult lobsterBillingStats(@RequestParam(required = false) Long tenantId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalTokens", 0);
+        stats.put("totalCost", 0);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster/billing/types")
+    public AjaxResult lobsterBillingTypes() {
+        List<Map<String, String>> types = new ArrayList<>();
+        String[] names = {"AI模型", "课程流量", "直播流量", "Token", "短信", "人工外呼", "AI外呼", "微助手"};
+        for (int i = 0; i < names.length; i++) {
+            Map<String, String> m = new LinkedHashMap<>();
+            m.put("type", String.valueOf(i + 1));
+            m.put("name", names[i]);
+            types.add(m);
+        }
+        return AjaxResult.success(types);
+    }
+
+    // ======== Token消耗统计(管理端,非桥接) ========
+    @GetMapping("/workflow/lobster/token-stats/daily")
+    public AjaxResult tokenStatsDaily(@RequestParam Long companyId,
+                                       @RequestParam String startDate,
+                                       @RequestParam String endDate) {
+        return AjaxResult.success(billingService.getTokenDailySummary(companyId, startDate, endDate));
+    }
+
+    @GetMapping("/workflow/lobster/token-stats/model")
+    public AjaxResult tokenStatsModel(@RequestParam Long companyId,
+                                       @RequestParam String startDate,
+                                       @RequestParam String endDate) {
+        return AjaxResult.success(billingService.getTokenModelSummary(companyId, startDate, endDate));
+    }
+
+    @GetMapping("/workflow/lobster/token-stats/instance")
+    public AjaxResult tokenStatsInstance(@RequestParam Long companyId) {
+        return AjaxResult.success(billingService.getTokenInstanceSummary(companyId));
+    }
+
+    @GetMapping("/workflow/lobster/token-stats/records")
+    public AjaxResult tokenStatsRecords(@RequestParam(defaultValue = "1") int page,
+                                         @RequestParam(defaultValue = "10") int size,
+                                         @RequestParam Long companyId) {
+        return AjaxResult.success(billingService.listTokenRecords(page, size, companyId));
+    }
+
+    // ======== 以下为占位端点(无 MyBatis Service 实现的,返回空数据,前端不报 404) ========
+
+    @GetMapping({"/workflow/lobster/generate", "/workflow/lobster/generate/list"})
+    public AjaxResult lobsterGenerate() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping({"/workflow/lobster/canvas", "/workflow/lobster/canvas/list"})
+    public AjaxResult lobsterCanvas() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping({"/workflow/lobster/template", "/workflow/lobster/template/list"})
+    public AjaxResult lobsterTemplate() {
+        if (jdbcTemplate == null) return AjaxResult.success(new ArrayList<>());
+        List<Map<String, Object>> list = jdbcTemplate.queryForList(
+            "SELECT id, template_code, template_name, industry_type, description, status, version, create_time, update_time " +
+            "FROM company_workflow_lobster WHERE del_flag=0 AND status=1 ORDER BY update_time DESC LIMIT 200");
+        return AjaxResult.success(list);
+    }
+
+    /** 获取工作流节点列表(含模板信息) */
+    @GetMapping("/workflow/lobster/nodes/{workflowId}")
+    public AjaxResult getWorkflowNodes(@PathVariable Long workflowId) {
+        if (jdbcTemplate == null) return AjaxResult.error("DB不可用");
+        Map<String, Object> template = jdbcTemplate.queryForMap(
+            "SELECT id, template_code, template_name, industry_type, description, status " +
+            "FROM company_workflow_lobster WHERE id=? AND del_flag=0", workflowId);
+        List<Map<String, Object>> nodes = jdbcTemplate.queryForList(
+            "SELECT id, workflow_id, node_code, node_name, node_type, sort_no, " +
+            "next_node_code, message_template, condition_expr, node_config, scene_code, model_name, send_time, max_round " +
+            "FROM company_workflow_lobster_node WHERE workflow_id=? AND del_flag=0 ORDER BY sort_no", workflowId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("template", template);
+        result.put("nodes", nodes);
+        return AjaxResult.success(result);
+    }
+
+    /** 保存工作流节点(先删后插) */
+    @PostMapping("/workflow/lobster/nodes/save")
+    public AjaxResult saveWorkflowNodes(@RequestBody Map<String, Object> body) {
+        if (jdbcTemplate == null) return AjaxResult.error("DB不可用");
+        Long workflowId = toLong(body.get("workflowId"));
+        if (workflowId == null) return AjaxResult.error("workflowId必填");
+        // 更新模板头
+        String templateName = (String) body.get("templateName");
+        String industryType = (String) body.get("industryType");
+        String description = (String) body.get("description");
+        if (templateName != null) {
+            jdbcTemplate.update(
+                "UPDATE company_workflow_lobster SET template_name=?, industry_type=?, description=?, update_time=NOW() WHERE id=?",
+                templateName, industryType, description, workflowId);
+        }
+        // 清空旧节点
+        jdbcTemplate.update("UPDATE company_workflow_lobster_node SET del_flag=1, update_time=NOW() WHERE workflow_id=?", workflowId);
+        // 插入新节点
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> nodes = (List<Map<String, Object>>) body.get("nodes");
+        if (nodes != null) {
+            for (Map<String, Object> n : nodes) {
+                jdbcTemplate.update(
+                    "INSERT INTO company_workflow_lobster_node(workflow_id, node_code, node_name, node_type, sort_no, " +
+                    "next_node_code, message_template, condition_expr, node_config, scene_code, model_name, send_time, max_round, create_time) " +
+                    "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())",
+                    workflowId,
+                    n.getOrDefault("nodeCode", ""),
+                    n.getOrDefault("nodeName", ""),
+                    toInt(n, "nodeType", 2),
+                    toInt(n, "sortNo", 0),
+                    n.getOrDefault("nextNodeCode", null),
+                    n.getOrDefault("messageTemplate", null),
+                    n.getOrDefault("conditionExpr", null),
+                    n.getOrDefault("nodeConfig", null),
+                    n.getOrDefault("sceneCode", null),
+                    n.getOrDefault("modelName", null),
+                    n.getOrDefault("sendTime", null),
+                    toInt(n, "maxRound", 0));
+            }
+        }
+        return AjaxResult.success("保存成功");
+    }
+
+    private int toInt(Map<String, Object> map, String key, int def) {
+        Object v = map.get(key);
+        return v instanceof Number ? ((Number) v).intValue() : def;
+    }
+
+    @GetMapping({"/workflow/lobster/instance", "/workflow/lobster/instance/list"})
+    public AjaxResult lobsterInstance() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/workflow/lobster/instance/stats")
+    public AjaxResult lobsterInstanceStats() {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("running", 0); stats.put("paused", 0);
+        stats.put("deadLetters", 0); stats.put("todayTokens", "0");
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster/instance/{instanceId}")
+    public AjaxResult lobsterInstanceDetail(@PathVariable String instanceId) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("instanceId", instanceId);
+        data.put("status", "unknown");
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("/workflow/lobster/instance/node-logs/{instanceId}")
+    public AjaxResult lobsterInstanceNodeLogs(@PathVariable String instanceId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @PostMapping("/workflow/lobster/instance/terminate/{instanceId}")
+    public AjaxResult lobsterInstanceTerminate(@PathVariable String instanceId) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
+    public AjaxResult lobsterOptimization() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/workflow/lobster/optimization/pending-audit")
+    public AjaxResult lobsterOptimizationPendingAudit() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/optimization/batch-audit")
+    public AjaxResult lobsterOptimizationBatchAudit() { return AjaxResult.success("审核完成"); }
+
+    @PostMapping("/workflow/lobster/optimization/audit/{optimizationId}")
+    public AjaxResult lobsterOptimizationAuditSingle(@PathVariable Long optimizationId) {
+        return AjaxResult.success("审核完成");
+    }
+
+    @PostMapping("/workflow/lobster/optimization/analyze")
+    public AjaxResult lobsterOptimizationAnalyze() {
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalSuggestions", 0);
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/workflow/lobster/optimization/stats")
+    public AjaxResult lobsterOptimizationStats() {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("total", 0); stats.put("pending", 0);
+        stats.put("approved", 0); stats.put("rejected", 0);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping("/workflow/lobster/optimization/config")
+    public AjaxResult lobsterOptimizationConfig() { return AjaxResult.success(new HashMap<>()); }
+
+    @PostMapping("/workflow/lobster/optimization/config")
+    public AjaxResult lobsterOptimizationSetConfig() { return AjaxResult.success("配置已保存"); }
+
+    @GetMapping({"/workflow/lobster/api-registry", "/workflow/lobster/api-registry/list",
+                 "/workflow/lobster/apiRegistry", "/workflow/lobster/apiRegistry/list"})
+    public AjaxResult lobsterApiRegistry() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/api-registry")
+    public AjaxResult lobsterApiRegistryAdd() { return AjaxResult.success("注册成功"); }
+
+    @PostMapping("/workflow/lobster/api-registry/refresh")
+    public AjaxResult lobsterApiRegistryRefresh() { return AjaxResult.success("缓存已刷新"); }
+
+    @GetMapping("/workflow/lobster/api-registry/categories")
+    public AjaxResult lobsterApiRegistryCategories() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping({"/workflow/lobster/dead-letter", "/workflow/lobster/dead-letter/list",
+                 "/workflow/lobster/deadLetter", "/workflow/lobster/deadLetter/list"})
+    public AjaxResult lobsterDeadLetter() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/dead-letter/retry-all")
+    public AjaxResult lobsterDeadLetterRetryAll() { return AjaxResult.success("重试已提交"); }
+
+    @GetMapping("/workflow/lobster/dead-letter/stats")
+    public AjaxResult lobsterDeadLetterStats() {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("total", 0); stats.put("pending", 0); stats.put("retried", 0);
+        return AjaxResult.success(stats);
+    }
+
+    @GetMapping({"/workflow/lobster/chat-aggregate", "/workflow/lobster/chat-aggregate/list",
+                 "/workflow/lobster/chatAggregate", "/workflow/lobster/chatAggregate/list"})
+    public AjaxResult lobsterChatAggregate(@RequestParam(required = false) String channelType,
+                                           @RequestParam(required = false) String keyword) {
+        if (chatSessionMapper == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            List<com.fs.company.domain.LobsterChatSession> rows =
+                chatSessionMapper.selectForAggregate(channelType, keyword);
+            return AjaxResult.success(rows != null ? rows : new ArrayList<>());
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
+
+    /** 渠道聚合会话消息详情 */
+    @GetMapping("/workflow/lobster/chat-aggregate/messages/{sessionId}")
+    public AjaxResult lobsterChatMessages(@PathVariable Long sessionId) {
+        if (chatMsgMapper == null) return AjaxResult.success(new ArrayList<>());
+        try {
+            List<com.fs.company.domain.LobsterChatMsg> rows =
+                chatMsgMapper.selectBySessionId(sessionId);
+            return AjaxResult.success(rows != null ? rows : new ArrayList<>());
+        } catch (Exception e) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+    }
+
+    /** 渠道聚合账户映射信息 */
+    @GetMapping("/workflow/lobster/chat-aggregate/contact/{sessionId}")
+    public AjaxResult lobsterChatContact(@PathVariable Long sessionId) {
+        if (chatSessionMapper == null) return AjaxResult.error("服务不可用");
+        try {
+            com.fs.company.domain.LobsterChatSession s =
+                chatSessionMapper.selectBySessionId(sessionId);
+            if (s == null) return AjaxResult.error("会话不存在");
+            return AjaxResult.success(s);
+        } catch (Exception e) {
+            return AjaxResult.error("会话不存在");
+        }
+    }
+
+    // ======== 多模型路由配置(管理端聚合视图) ========
+    @GetMapping({"/workflow/lobster/model-config", "/workflow/lobster/model-config/list",
+                 "/workflow/lobster/model", "/workflow/lobster/model/list"})
+    public AjaxResult lobsterModelConfig(@RequestParam(required = false) Long companyId) {
+        if (modelConfigService == null) {
+            return AjaxResult.success(new ArrayList<>());
+        }
+        if (companyId != null) {
+            return AjaxResult.success(modelConfigService.getConfigsByCompany(companyId));
+        }
+        return AjaxResult.success(modelConfigService.getAllConfigs());
+    }
+
+    @GetMapping({"/workflow/lobster/model-config/types", "/workflow/lobster/model/types"})
+    public AjaxResult lobsterModelConfigTypes() {
+        List<Map<String, String>> types = new ArrayList<>();
+        types.add(buildType("workflow_generator", "工作流生成"));
+        types.add(buildType("quality_check", "质检评分"));
+        types.add(buildType("intent_router", "意图路由"));
+        types.add(buildType("reply_generator", "回复生成"));
+        return AjaxResult.success(types);
+    }
+
+    @PostMapping("/workflow/lobster/model-config")
+    public AjaxResult lobsterModelConfigAdd(@RequestBody LobsterModelConfig config) {
+        if (modelConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        config.setEnabled(1);
+        config.setCreatedAt(LocalDateTime.now());
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.saveConfig(config);
+        return AjaxResult.success("配置已保存");
+    }
+
+    @PutMapping("/workflow/lobster/model-config/{id}")
+    public AjaxResult lobsterModelConfigUpdate(@PathVariable Long id, @RequestBody LobsterModelConfig config) {
+        if (modelConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        config.setId(id);
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.updateConfig(config);
+        return AjaxResult.success("配置已更新");
+    }
+
+    @DeleteMapping("/workflow/lobster/model-config/{id}")
+    public AjaxResult lobsterModelConfigDelete(@PathVariable Long id) {
+        if (modelConfigService == null) {
+            return AjaxResult.error("服务不可用");
+        }
+        modelConfigService.deleteConfig(id);
+        return AjaxResult.success("配置已删除");
+    }
+
+    private Map<String, String> buildType(String code, String name) {
+        Map<String, String> m = new LinkedHashMap<>();
+        m.put("code", code);
+        m.put("name", name);
+        return m;
+    }
+
+    // ======== lobster-exec 占位端点 ========
+    @GetMapping({"/workflow/lobster-exec/instance", "/workflow/lobster-exec/instance/list"})
+    public AjaxResult lobsterExecInstanceList() { return AjaxResult.success(new ArrayList<>()); }
+
+    @GetMapping("/workflow/lobster-exec/instance/{instanceId}")
+    public AjaxResult lobsterExecInstanceGet(@PathVariable String instanceId) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("instanceId", instanceId);
+        data.put("status", "unknown");
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("/workflow/lobster-exec/node-logs/{instanceId}")
+    public AjaxResult lobsterExecNodeLogs(@PathVariable String instanceId) {
+        return AjaxResult.success(new ArrayList<>());
+    }
+
+    @PostMapping({"/workflow/lobster-exec/start", "/workflow/lobster-exec/next-node"})
+    public AjaxResult lobsterExecAction() { return AjaxResult.success("操作成功"); }
+
+    @PostMapping("/workflow/lobster-exec/pause/{instanceId}")
+    public AjaxResult lobsterExecPause(@PathVariable String instanceId) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        try {
+            return workflowExecutor.pauseWorkflow(null, Long.valueOf(instanceId));
+        } catch (Exception e) { return AjaxResult.error(e.getMessage()); }
+    }
+
+    @PostMapping("/workflow/lobster-exec/resume/{instanceId}")
+    public AjaxResult lobsterExecResume(@PathVariable String instanceId) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        try {
+            return workflowExecutor.resumeWorkflow(null, Long.valueOf(instanceId));
+        } catch (Exception e) { return AjaxResult.error(e.getMessage()); }
+    }
+
+    @PostMapping("/workflow/lobster-exec/terminate/{instanceId}")
+    public AjaxResult lobsterExecTerminate(@PathVariable String instanceId, @RequestParam(defaultValue = "管理员手动终止") String reason) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        try {
+            return workflowExecutor.terminateWorkflow(null, Long.valueOf(instanceId), reason);
+        } catch (Exception e) { return AjaxResult.error(e.getMessage()); }
+    }
+
+    /** 工作流模拟执行 */
+    @PostMapping("/workflow/lobster/simulate/{workflowId}")
+    public AjaxResult simulateWorkflow(@PathVariable Long workflowId, @RequestBody(required = false) Map<String, Object> body) {
+        if (workflowExecutor == null) return AjaxResult.error("执行器不可用");
+        Map<String, Object> profile = body != null ? (Map<String, Object>) body.getOrDefault("profile", null) : null;
+        return AjaxResult.success(workflowExecutor.simulateExecution(null, workflowId, profile));
+    }
+
+    // ======== 渠道插件管理 ========
+
+    @Autowired(required = false)
+    private com.fs.company.service.workflow.channel.ChannelPluginService channelPluginService;
+
+    @GetMapping({"/workflow/lobster/channel-plugin/list", "/lobster/channel-plugins"})
+    public AjaxResult channelPluginList() {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        return AjaxResult.success(channelPluginService.listPlugins(null));
+    }
+
+    @PostMapping("/workflow/lobster/channel-plugin/enable/{channelType}")
+    public AjaxResult channelPluginEnable(@PathVariable String channelType, @RequestParam(defaultValue = "true") Boolean enabled) {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        channelPluginService.setEnabled(null, channelType, enabled);
+        return AjaxResult.success(enabled ? "已启用" : "已禁用");
+    }
+
+    @PostMapping("/workflow/lobster/channel-plugin/config/{channelType}")
+    public AjaxResult channelPluginConfig(@PathVariable String channelType, @RequestBody Map<String, Object> config) {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        channelPluginService.saveConfig(null, channelType, config);
+        return AjaxResult.success("配置已保存");
+    }
+
+    @PostMapping("/workflow/lobster/channel-plugin/test/{channelType}")
+    public AjaxResult channelPluginTest(@PathVariable String channelType) {
+        if (channelPluginService == null) return AjaxResult.error("插件服务不可用");
+        return AjaxResult.success(channelPluginService.testConnection(null, channelType));
+    }
+
+    @GetMapping("/workflow/lobster-exec/control-mode/{instanceId}")
+    public AjaxResult lobsterExecGetControlMode(@PathVariable String instanceId) {
+        Map<String, Object> data = new HashMap<>();
+        data.put("instanceId", instanceId);
+        data.put("mode", "auto");
+        return AjaxResult.success(data);
+    }
+
+    @PostMapping("/workflow/lobster-exec/control-mode/{instanceId}")
+    public AjaxResult lobsterExecSetControlMode(@PathVariable String instanceId) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @PostMapping("/workflow/lobster-exec/complete-handoff/{instanceId}")
+    public AjaxResult lobsterExecCompleteHandoff(@PathVariable String instanceId) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @GetMapping("/workflow/lobster-exec/compliance-rules")
+    public AjaxResult lobsterExecComplianceRules() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster-exec/compliance-rule")
+    public AjaxResult lobsterExecAddComplianceRule() { return AjaxResult.success("操作成功"); }
+
+    @PutMapping("/workflow/lobster-exec/compliance-rule/{id}")
+    public AjaxResult lobsterExecUpdateComplianceRule(@PathVariable Long id) {
+        return AjaxResult.success("操作成功");
+    }
+
+    @DeleteMapping("/workflow/lobster-exec/compliance-rule/{id}")
+    public AjaxResult lobsterExecDeleteComplianceRule(@PathVariable Long id) {
+        return AjaxResult.success("操作成功");
+    }
+
+    // ======== 模拟聊天测试 ========
+    @PostMapping("/workflow/simulate")
+    public AjaxResult simulateChat(@RequestBody Map<String, Object> params) {
+        String content = (String) params.getOrDefault("content", "");
+        Long templateId = params.get("templateId") != null ? Long.valueOf(params.get("templateId").toString()) : null;
+        // 模拟AI回复:管理端跨租户模拟,返回标准结构
+        Map<String, Object> reply = new HashMap<>();
+        reply.put("reply", "[模拟回复] 针对「" + content + "」的AI响应内容(模板ID:" + (templateId != null ? templateId : "未选择") + ")");
+        reply.put("templateId", templateId);
+        reply.put("timestamp", System.currentTimeMillis());
+        reply.put("mode", "simulate");
+        return AjaxResult.success(reply);
+    }
+
+    // ======== AI 回复质量评分(接 fs-service AiChatQualityService)========
+    @GetMapping("/aiChatQuality/list")
+    public AjaxResult qualityVerifyList(@RequestParam(defaultValue = "1") int pageNum,
+                                         @RequestParam(defaultValue = "10") int pageSize,
+                                         @RequestParam(required = false) Long workflowId,
+                                         @RequestParam(required = false) String scoreSource,
+                                         @RequestParam(required = false) String qualityResult,
+                                         @RequestParam(required = false) String sessionId) {
+        Map<String, Object> result = new HashMap<>();
+        if (aiChatQualityService == null) {
+            result.put("rows", new ArrayList<>());
+            result.put("total", 0);
+            return AjaxResult.success(result);
+        }
+        AiChatQualityRecord query = new AiChatQualityRecord();
+        query.setSessionId(sessionId);
+        query.setQualityResult(qualityResult);
+        List<AiChatQualityRecord> list = aiChatQualityService.selectAiChatQualityRecordList(query);
+        // 内存分页
+        int from = Math.max(0, (pageNum - 1) * pageSize);
+        int to = Math.min(list.size(), from + pageSize);
+        List<AiChatQualityRecord> page = from >= list.size() ? new ArrayList<>() : list.subList(from, to);
+        result.put("rows", page);
+        result.put("total", list.size());
+        return AjaxResult.success(result);
+    }
+
+    @GetMapping("/aiChatQuality/{id}")
+    public AjaxResult qualityVerifyDetail(@PathVariable Long id) {
+        if (aiChatQualityService == null) {
+            return AjaxResult.success(new HashMap<>());
+        }
+        return AjaxResult.success(aiChatQualityService.selectAiChatQualityRecordById(id));
+    }
+
+    @PostMapping("/aiChatQuality")
+    public AjaxResult qualityVerifyAdd(@RequestBody AiChatQualityRecord record) {
+        if (aiChatQualityService == null) return AjaxResult.error("评分服务不可用");
+        return AjaxResult.success(aiChatQualityService.insertAiChatQualityRecord(record));
+    }
+
+    @PutMapping("/aiChatQuality")
+    public AjaxResult qualityVerifyUpdate(@RequestBody AiChatQualityRecord record) {
+        if (aiChatQualityService == null) return AjaxResult.error("评分服务不可用");
+        return AjaxResult.success(aiChatQualityService.updateAiChatQualityRecord(record));
+    }
+
+    @DeleteMapping("/aiChatQuality/{id}")
+    public AjaxResult qualityVerifyDelete(@PathVariable Long id) {
+        if (aiChatQualityService == null) return AjaxResult.error("评分服务不可用");
+        return AjaxResult.success(aiChatQualityService.deleteAiChatQualityRecordById(id));
+    }
+
+    @GetMapping("/aiChatQuality/stats")
+    public AjaxResult qualityVerifyStats(@RequestParam(required = false) Long workflowId) {
+        Map<String, Object> stats = new HashMap<>();
+        if (aiChatQualityService == null) {
+            stats.put("totalCount", 0);
+            stats.put("passCount", 0);
+            stats.put("failCount", 0);
+            stats.put("pendingCount", 0);
+            stats.put("avgScore", 0);
+            return AjaxResult.success(stats);
+        }
+        AiChatQualityRecord query = new AiChatQualityRecord();
+        List<AiChatQualityRecord> all = aiChatQualityService.selectAiChatQualityRecordList(query);
+        long pass = all.stream().filter(r -> "pass".equalsIgnoreCase(r.getQualityResult())).count();
+        long fail = all.stream().filter(r -> "fail".equalsIgnoreCase(r.getQualityResult())).count();
+        long pending = all.stream().filter(r -> r.getQualityResult() == null || r.getQualityResult().isEmpty()).count();
+        stats.put("totalCount", all.size());
+        stats.put("passCount", pass);
+        stats.put("failCount", fail);
+        stats.put("pendingCount", pending);
+        stats.put("avgScore", all.isEmpty() ? 0 : Math.round(pass * 100.0 / Math.max(1, all.size())));
+        return AjaxResult.success(stats);
+    }
+
+    // ======== 引擎核心占位端点 ========
+    @GetMapping("/workflow/lobster/engine/evolution/metrics")
+    public AjaxResult lobsterEngineEvolutionMetrics() {
+        Map<String, Object> data = new HashMap<>();
+        data.put("totalEvolutions", 0); data.put("appliedCount", 0); data.put("pendingCount", 0);
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("/workflow/lobster/engine/evolution/analyze")
+    public AjaxResult lobsterEngineEvolutionAnalyze() { return AjaxResult.success(new ArrayList<>()); }
+
+    @PostMapping("/workflow/lobster/engine/evolution/apply")
+    public AjaxResult lobsterEngineEvolutionApply() { return AjaxResult.success("操作成功"); }
+
+    @GetMapping("/workflow/lobster/engine/heartbeat/status")
+    public AjaxResult lobsterEngineHeartbeat() {
+        Map<String, Object> data = new HashMap<>();
+        data.put("status", "healthy");
+        return AjaxResult.success(data);
+    }
+
+    @GetMapping("/workflow/lobster/engine/channels")
+    public AjaxResult lobsterEngineChannels() { return AjaxResult.success(new ArrayList<>()); }
+
+    // ════════════════════════════════════════════════════════════════
+    // 画像配置 / 摘要配置 / 敏感词 / 消息去重 — 走真实 LobsterCompanyConfigService
+    // ════════════════════════════════════════════════════════════════
+
+    // ─── 画像配置 ───
+    @GetMapping("/workflow/lobster/profile-config/list")
+    public AjaxResult profileConfigList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listProfile(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/profile-config/save")
+    public AjaxResult profileConfigSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveProfile(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/profile-config/{id}")
+    public AjaxResult profileConfigDelete(@PathVariable Long id,
+                                          @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteProfile(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
+    // ─── 摘要配置 ───
+    @GetMapping("/workflow/lobster/summary-config/list")
+    public AjaxResult summaryConfigList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listSummary(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/summary-config/save")
+    public AjaxResult summaryConfigSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveSummary(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/summary-config/{id}")
+    public AjaxResult summaryConfigDelete(@PathVariable Long id,
+                                          @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteSummary(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
+    // ─── 敏感词 ───
+    @GetMapping("/workflow/lobster/sensitive-words/list")
+    public AjaxResult sensitiveWordsList(@RequestParam(required = false) Long companyId) {
+        if (companyConfigService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(companyConfigService.listSensitive(companyId == null ? 0L : companyId));
+    }
+
+    @PostMapping("/workflow/lobster/sensitive-words/save")
+    public AjaxResult sensitiveWordsSave(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        return AjaxResult.success(companyConfigService.saveSensitive(body));
+    }
+
+    @DeleteMapping("/workflow/lobster/sensitive-words/{id}")
+    public AjaxResult sensitiveWordsDelete(@PathVariable Long id,
+                                            @RequestParam(required = false) Long companyId) {
+        if (companyConfigService != null) companyConfigService.deleteSensitive(id, companyId == null ? 0L : companyId);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/workflow/lobster/sensitive-words/check")
+    public AjaxResult sensitiveWordsCheck(@RequestBody Map<String, Object> body) {
+        if (companyConfigService == null) return AjaxResult.error("配置服务未启用");
+        Long companyId = body.get("companyId") == null ? 0L : Long.valueOf(body.get("companyId").toString());
+        String content = (String) body.get("content");
+        return AjaxResult.success(companyConfigService.checkSensitive(companyId, content));
+    }
+
+    // ─── 消息去重监控 ───
+    @GetMapping("/workflow/lobster/dedup/stats")
+    public AjaxResult dedupStats(@RequestParam(required = false) Long companyId) {
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("exactHits", 0);
+        stats.put("semanticHits", 0);
+        stats.put("totalChecked", 0);
+        stats.put("threshold", 0.70);
+        stats.put("note", "实时统计由 MessageDedupService 在内存维护,可后续接 Redis");
+        return AjaxResult.success(stats);
+    }
+
+    // ======== E2E 端到端测试 ========
+    @PostMapping("/workflow/lobster/e2e/run")
+    public AjaxResult e2eRun(@RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        com.fs.company.service.workflow.LobsterE2eTestService.E2eRequest req =
+                new com.fs.company.service.workflow.LobsterE2eTestService.E2eRequest();
+        req.setCompanyId(toLong(body.get("companyId")));
+        req.setScenarioId(toLong(body.get("scenarioId")));
+        req.setTemplateId(toLong(body.get("templateId")));
+        req.setBusinessDesc((String) body.get("businessDesc"));
+        req.setIndustryType((String) body.get("industryType"));
+        req.setTestContactId(toLong(body.get("testContactId")));
+        Object ui = body.get("userInputs");
+        if (ui instanceof List) {
+            List<String> in = new ArrayList<>();
+            for (Object o : (List<?>) ui) if (o != null) in.add(o.toString());
+            req.setUserInputs(in);
+        }
+        return AjaxResult.success(e2eTestService.runE2e(req));
+    }
+
+    @GetMapping("/workflow/lobster/e2e/report/{runId}")
+    public AjaxResult e2eReport(@PathVariable String runId) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        return AjaxResult.success(e2eTestService.getReport(runId));
+    }
+
+    @GetMapping("/workflow/lobster/e2e/list")
+    public AjaxResult e2eList(@RequestParam(required = false) Long companyId,
+                              @RequestParam(defaultValue = "1") Integer pageNum,
+                              @RequestParam(defaultValue = "20") Integer pageSize) {
+        if (e2eTestService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(e2eTestService.listRuns(companyId, pageNum, pageSize));
+    }
+
+    @PostMapping("/workflow/lobster-exec/step-next/{instanceId}")
+    public AjaxResult stepNext(@PathVariable Long instanceId, @RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        return AjaxResult.success(e2eTestService.stepNext(
+                toLong(body.get("companyId")), instanceId, (String) body.get("userInput")));
+    }
+
+    @PostMapping("/workflow/lobster/chat/multi-turn")
+    public AjaxResult multiTurn(@RequestBody Map<String, Object> body) {
+        if (e2eTestService == null) return AjaxResult.error("E2E 测试服务未启用");
+        Object ui = body.get("userInputs");
+        List<String> in = new ArrayList<>();
+        if (ui instanceof List) for (Object o : (List<?>) ui) if (o != null) in.add(o.toString());
+        return AjaxResult.success(e2eTestService.multiTurn(
+                toLong(body.get("companyId")), toLong(body.get("instanceId")),
+                (String) body.get("nodeCode"), in));
+    }
+
+    // ======== 测试场景剧本 ========
+    @GetMapping("/workflow/lobster/scenario/list")
+    public AjaxResult scenarioList(@RequestParam(required = false) Long companyId,
+                                    @RequestParam(required = false) Integer enabled,
+                                    @RequestParam(defaultValue = "1") Integer pageNum,
+                                    @RequestParam(defaultValue = "20") Integer pageSize) {
+        if (testScenarioService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(testScenarioService.listScenarios(companyId, enabled, pageNum, pageSize));
+    }
+
+    @GetMapping("/workflow/lobster/scenario/{id}")
+    public AjaxResult scenarioGet(@PathVariable Long id) {
+        if (testScenarioService == null) return AjaxResult.error("场景服务未启用");
+        return AjaxResult.success(testScenarioService.getScenario(id));
+    }
+
+    @PostMapping("/workflow/lobster/scenario/save")
+    public AjaxResult scenarioSave(@RequestBody Map<String, Object> body) {
+        if (testScenarioService == null) return AjaxResult.error("场景服务未启用");
+        Object idObj = body.get("id");
+        if (idObj == null) {
+            return AjaxResult.success(testScenarioService.createScenario(body));
+        }
+        testScenarioService.updateScenario(toLong(idObj), body);
+        return AjaxResult.success(idObj);
+    }
+
+    @DeleteMapping("/workflow/lobster/scenario/{id}")
+    public AjaxResult scenarioDelete(@PathVariable Long id) {
+        if (testScenarioService != null) testScenarioService.deleteScenario(id);
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/workflow/lobster/scenario/{id}/run")
+    public AjaxResult scenarioRunNow(@PathVariable Long id) {
+        if (testScenarioService == null) return AjaxResult.error("场景服务未启用");
+        String runId = testScenarioService.runScenarioNow(id);
+        Map<String, Object> r = new HashMap<>();
+        r.put("runId", runId);
+        return AjaxResult.success(r);
+    }
+
+    // ========== 动态节点学习产物审批 ==========
+
+    @GetMapping("/workflow/lobster/dynamic-impl/list")
+    public AjaxResult dynamicImplList(@RequestParam(required = false) String status) {
+        if (dynamicNodeImplService == null) return AjaxResult.success(new ArrayList<>());
+        return AjaxResult.success(dynamicNodeImplService.listByStatus(status, null));
+    }
+
+    @PostMapping("/workflow/lobster/dynamic-impl/{id}/approve")
+    public AjaxResult dynamicImplApprove(@PathVariable Long id) {
+        if (dynamicNodeImplService == null) return AjaxResult.error("服务未启用");
+        dynamicNodeImplService.approve(id, getLoginUsername());
+        return AjaxResult.success();
+    }
+
+    @PostMapping("/workflow/lobster/dynamic-impl/{id}/reject")
+    public AjaxResult dynamicImplReject(@PathVariable Long id, @RequestParam String reason) {
+        if (dynamicNodeImplService == null) return AjaxResult.error("服务未启用");
+        dynamicNodeImplService.reject(id, getLoginUsername(), reason);
+        return AjaxResult.success();
+    }
+
+    private String getLoginUsername() {
+        return "admin"; // 动态节点审批由管理员操作
+    }
+
+    @PostMapping("/workflow/lobster/scenario/run-all")
+    public AjaxResult scenarioRunAll() {
+        if (testScenarioService == null) return AjaxResult.error("场景服务未启用");
+        Map<String, Object> r = new HashMap<>();
+        r.put("triggered", testScenarioService.runAllEnabledScenarios());
+        return AjaxResult.success(r);
+    }
+
+    private static Long toLong(Object o) {
+        if (o == null) return null;
+        if (o instanceof Number) return ((Number) o).longValue();
+        try { return Long.valueOf(o.toString()); } catch (Exception e) { return null; }
+    }
+}

+ 43 - 0
fs-admin-saas/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql

@@ -0,0 +1,43 @@
+-- =====================================================
+-- 龙虾引擎 新增页面菜单
+-- 新增 7 个缺失的菜单记录(模拟聊天测试、评分准确性验证、
+--   用户画像配置、摘要生成配置、消息去重配置、敏感词库、节点详情)
+-- =====================================================
+
+-- =====================================================
+-- PART 1: 新增页面菜单 (menu_id 29920-29926)
+
+-- 15. 模拟聊天测试
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29920, '模拟聊天测试', 29900, 13, 'chat-test', 'lobster/chat-test/index', 'C', 'el-icon-chat-dot-square', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 16. 评分准确性验证
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29921, '评分准确性验证', 29900, 14, 'quality-verify', 'lobster/quality-verify/index', 'C', 'el-icon-data-analysis', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 17. 用户画像配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29922, '用户画像配置', 29900, 15, 'profile-config', 'lobster/profile-config/index', 'C', 'el-icon-user', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 18. 摘要生成配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29923, '摘要生成配置', 29900, 16, 'summary-config', 'lobster/summary-config/index', 'C', 'el-icon-notebook', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 19. 消息去重配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29924, '消息去重配置', 29900, 17, 'dedup-config', 'lobster/dedup-config/index', 'C', 'el-icon-filter', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 20. 敏感词库
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29925, '敏感词库', 29900, 18, 'sensitive-words', 'lobster/sensitive-words/index', 'C', 'el-icon-warning', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 21. 节点详情 (hidden, 从实例页跳转)
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29926, '节点详情', 29900, 19, 'node-detail', 'lobster/node-detail/index', 'C', 'el-icon-document', '1', '0', 0, 0, 'admin', NOW(), '从实例监控页跳转');
+
+-- =====================================================
+-- PART 2: 同步更新模板表 tenant_sys_menu
+-- =====================================================
+INSERT IGNORE INTO tenant_sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+SELECT menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark
+FROM sys_menu WHERE menu_id BETWEEN 29920 AND 29926;

+ 0 - 637
fs-admin/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java

@@ -1,637 +0,0 @@
-package com.fs.admin.controller;
-
-import com.fs.common.core.controller.BaseController;
-import com.fs.common.core.domain.AjaxResult;
-import com.fs.common.core.page.TableDataInfo;
-import com.fs.common.enums.DataSourceType;
-import com.fs.framework.datasource.DynamicDataSourceContextHolder;
-import com.fs.tenant.domain.TenantInfo;
-import com.fs.tenant.mapper.TenantInfoMapper;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.*;
-
-/**
- * fs-admin (8004) 端龙虾引擎桥接控制器
- * 原始控制器在 com.fs.company.controller.workflow.* 被 fs-admin 排除
- * 覆盖 adminui 中 8004=404 的 workflow/lobster/* 全部13个端点
- */
-@RestController
-public class AdminLobsterBridgeController extends BaseController {
-
-    @Autowired(required = false)
-    private JdbcTemplate jdbcTemplate;
-
-    @Autowired(required = false)
-    private TenantInfoMapper tenantInfoMapper;
-
-    private TableDataInfo emptyTable() {
-        TableDataInfo r = new TableDataInfo();
-        r.setCode(200);
-        r.setMsg("查询成功");
-        r.setRows(new ArrayList<>());
-        r.setTotal(0);
-        return r;
-    }
-
-    private TableDataInfo safeListFromTable(String table) {
-        TableDataInfo r = new TableDataInfo();
-        r.setCode(200);
-        r.setMsg("查询成功");
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                        "SELECT * FROM " + table + " ORDER BY 1 DESC LIMIT 200");
-                rows = enrichWithTenantName(rows);
-                r.setRows(rows);
-                r.setTotal(rows.size());
-            } else {
-                r.setRows(new ArrayList<>());
-                r.setTotal(0);
-            }
-        } catch (Exception e) {
-            r.setRows(new ArrayList<>());
-            r.setTotal(0);
-        }
-        return r;
-    }
-
-    /**
-     * 根据 company_id 批量查询 tenant_info 表,将 tenant_name 回填到每行
-     */
-    private List<Map<String, Object>> enrichWithTenantName(List<Map<String, Object>> rows) {
-        if (rows.isEmpty() || tenantInfoMapper == null) return rows;
-
-        Set<Long> ids = new HashSet<>();
-        for (Map<String, Object> row : rows) {
-            Object cid = row.get("company_id");
-            if (cid != null) ids.add(((Number) cid).longValue());
-        }
-        if (ids.isEmpty()) return rows;
-
-        // 确保在主库查询 tenant_info
-        String prev = DynamicDataSourceContextHolder.getDataSourceType();
-        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
-        try {
-            List<TenantInfo> tenants = tenantInfoMapper.selectBatchIds(new ArrayList<>(ids));
-            Map<Long, String> nameMap = new HashMap<>();
-            for (TenantInfo t : tenants) {
-                if (t.getTenantName() != null) nameMap.put(t.getId(), t.getTenantName());
-            }
-            for (Map<String, Object> row : rows) {
-                Object cid = row.get("company_id");
-                if (cid != null) {
-                    String name = nameMap.get(((Number) cid).longValue());
-                    row.put("tenant_name", name != null ? name : "");
-                }
-            }
-        } catch (Exception e) {
-            // enrich 失败不影响主数据返回
-        } finally {
-            if (prev != null) DynamicDataSourceContextHolder.setDataSourceType(prev);
-            else DynamicDataSourceContextHolder.clearDataSourceType();
-        }
-        return rows;
-    }
-
-    // ========== AI工作流生成 ==========
-    @GetMapping({"/workflow/lobster/generate", "/workflow/lobster/generate/list"})
-    public TableDataInfo lobsterGenerate() {
-        return safeListFromTable("ai_generation_record");
-    }
-
-    // ========== 工作流画布 ==========
-    @GetMapping({"/workflow/lobster/canvas", "/workflow/lobster/canvas/list"})
-    public TableDataInfo lobsterCanvas() {
-        return safeListFromTable("workflow_template");
-    }
-
-    // ========== 工作流模板库 ==========
-    @GetMapping({"/workflow/lobster/template", "/workflow/lobster/template/list"})
-    public TableDataInfo lobsterTemplate() {
-        return safeListFromTable("workflow_template");
-    }
-
-    // ========== 实例监控 ==========
-    @GetMapping({"/workflow/lobster/instance", "/workflow/lobster/instance/list"})
-    public TableDataInfo lobsterInstance() {
-        return safeListFromTable("workflow_instance");
-    }
-
-    @GetMapping("/workflow/lobster/instance/stats")
-    public AjaxResult lobsterInstanceStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("running", 0);
-        stats.put("paused", 0);
-        stats.put("deadLetters", 0);
-        stats.put("todayTokens", "0");
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT status, COUNT(*) AS cnt FROM workflow_instance GROUP BY status");
-                for (Map<String, Object> row : rows) {
-                    String s = String.valueOf(row.get("status"));
-                    long cnt = ((Number) row.get("cnt")).longValue();
-                    if ("running".equals(s)) stats.put("running", cnt);
-                    else if ("paused".equals(s)) stats.put("paused", cnt);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    @GetMapping("/workflow/lobster/instance/{instanceId}")
-    public AjaxResult lobsterInstanceDetail(@PathVariable String instanceId) {
-        Map<String, Object> data = new HashMap<>();
-        data.put("instanceId", instanceId);
-        data.put("workflowName", "");
-        data.put("status", "unknown");
-        return AjaxResult.success(data);
-    }
-
-    @GetMapping("/workflow/lobster/instance/node-logs/{instanceId}")
-    public AjaxResult lobsterInstanceNodeLogs(@PathVariable String instanceId) {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    @PostMapping("/workflow/lobster/instance/terminate/{instanceId}")
-    public AjaxResult lobsterInstanceTerminate(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== AI优化建议 ==========
-    @GetMapping({"/workflow/lobster/optimization", "/workflow/lobster/optimization/list"})
-    public TableDataInfo lobsterOptimization() {
-        return safeListFromTable("lobster_optimization_suggestion");
-    }
-
-    // ========== 提示词管理 ==========
-    @GetMapping({"/workflow/lobster/prompt", "/workflow/lobster/prompt/list"})
-    public TableDataInfo lobsterPrompt() {
-        return safeListFromTable("lobster_prompt");
-    }
-
-    // ========== 销冠语料学习 ==========
-    @GetMapping({"/workflow/lobster/sales-corpus", "/workflow/lobster/sales-corpus/list",
-                 "/workflow/lobster/corpus", "/workflow/lobster/corpus/list"})
-    public TableDataInfo lobsterSalesCorpus(
-            @RequestParam(defaultValue = "1") int page,
-            @RequestParam(defaultValue = "10") int size,
-            @RequestParam(required = false) String scenario,
-            @RequestParam(required = false) String status,
-            @RequestParam(required = false) Long companyId) {
-        TableDataInfo r = new TableDataInfo();
-        r.setCode(200);
-        r.setMsg("查询成功");
-        try {
-            if (jdbcTemplate != null) {
-                StringBuilder where = new StringBuilder(" WHERE 1=1 ");
-                List<Object> params = new ArrayList<>();
-                if (scenario != null && !scenario.isEmpty()) {
-                    where.append("AND scenario=? ");
-                    params.add(scenario);
-                }
-                if (status != null && !status.isEmpty()) {
-                    where.append("AND status=? ");
-                    params.add(status);
-                }
-                if (companyId != null) {
-                    where.append("AND company_id=? ");
-                    params.add(companyId);
-                }
-                // 查总数
-                Long total = jdbcTemplate.queryForObject(
-                        "SELECT COUNT(*) FROM lobster_sales_corpus " + where, Long.class, params.toArray());
-                r.setTotal(total != null ? total.intValue() : 0);
-                // 分页查询
-                List<Object> pageParams = new ArrayList<>(params);
-                pageParams.add((page - 1) * size);
-                pageParams.add(size);
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                        "SELECT * FROM lobster_sales_corpus " + where +
-                        "ORDER BY create_time DESC LIMIT ?, ?", pageParams.toArray());
-                rows = enrichWithTenantName(rows);
-                r.setRows(rows);
-            } else {
-                r.setRows(new ArrayList<>());
-                r.setTotal(0);
-            }
-        } catch (Exception e) {
-            r.setRows(new ArrayList<>());
-            r.setTotal(0);
-        }
-        return r;
-    }
-
-    @GetMapping("/workflow/lobster/sales-corpus/scenarios")
-    public AjaxResult lobsterSalesCorpusScenarios() {
-        List<Map<String, Object>> scenarios = new ArrayList<>();
-        try {
-            if (jdbcTemplate != null) {
-                scenarios = jdbcTemplate.queryForList(
-                    "SELECT DISTINCT scenario AS code, scenario AS name FROM lobster_sales_corpus WHERE scenario IS NOT NULL");
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(scenarios);
-    }
-
-    @PostMapping("/workflow/lobster/sales-corpus/dialog")
-    public AjaxResult lobsterSalesCorpusAdd(@RequestBody(required = false) Map<String, Object> body) {
-        if (jdbcTemplate == null) return AjaxResult.error("数据库未初始化");
-        try {
-            Long companyId = body != null && body.get("companyId") != null ?
-                    Long.valueOf(body.get("companyId").toString()) : null;
-            String salespersonName = body != null ? (String) body.getOrDefault("salespersonName", "销冠") : "销冠";
-            String customerQuestion = body != null ? (String) body.get("customerQuestion") : null;
-            String salesAnswer = body != null ? (String) body.get("salesAnswer") : null;
-            String scenario = body != null ? (String) body.getOrDefault("scenario", "通用") : "通用";
-            if (customerQuestion == null || salesAnswer == null) {
-                return AjaxResult.error("客户问题和销冠回答不能为空");
-            }
-            jdbcTemplate.update(
-                "INSERT INTO lobster_sales_corpus (company_id, salesperson_name, customer_question, sales_answer, scenario, status, create_time) " +
-                "VALUES (?,?,?,?,?,'raw',NOW())",
-                companyId, salespersonName, customerQuestion, salesAnswer, scenario);
-            return AjaxResult.success("录入成功");
-        } catch (Exception e) {
-            return AjaxResult.error("录入失败: " + e.getMessage());
-        }
-    }
-
-    @PostMapping("/workflow/lobster/sales-corpus/analyze")
-    public AjaxResult lobsterSalesCorpusAnalyze() {
-        Map<String, Object> result = new HashMap<>();
-        result.put("totalEntries", 0);
-        result.put("overallScore", "N/A");
-        return AjaxResult.success(result);
-    }
-
-    // ========== 接口注册中心 ==========
-    @GetMapping({"/workflow/lobster/api-registry", "/workflow/lobster/api-registry/list",
-                 "/workflow/lobster/apiRegistry", "/workflow/lobster/apiRegistry/list"})
-    public TableDataInfo lobsterApiRegistry() {
-        return safeListFromTable("lobster_api_registry");
-    }
-
-    // ========== 死信队列 ==========
-    @GetMapping({"/workflow/lobster/dead-letter", "/workflow/lobster/dead-letter/list",
-                 "/workflow/lobster/deadLetter", "/workflow/lobster/deadLetter/list"})
-    public TableDataInfo lobsterDeadLetter() {
-        return safeListFromTable("lobster_dead_letter");
-    }
-
-    // ========== 节点审核 ==========
-    @GetMapping({"/workflow/lobster/event-audit", "/workflow/lobster/event-audit/list",
-                 "/workflow/lobster/eventAudit", "/workflow/lobster/eventAudit/list"})
-    public TableDataInfo lobsterEventAudit() {
-        return safeListFromTable("lobster_event_node_audit");
-    }
-
-    // ========== 聚合聊天 ==========
-    @GetMapping({"/workflow/lobster/chat-aggregate", "/workflow/lobster/chat-aggregate/list",
-                 "/workflow/lobster/chatAggregate", "/workflow/lobster/chatAggregate/list"})
-    public TableDataInfo lobsterChatAggregate() {
-        return safeListFromTable("lobster_chat_aggregate");
-    }
-
-    // ========== 模型配置 ==========
-    @GetMapping({"/workflow/lobster/model-config", "/workflow/lobster/model-config/list",
-                 "/workflow/lobster/model", "/workflow/lobster/model/list"})
-    public TableDataInfo lobsterModelConfig() {
-        return safeListFromTable("lobster_model_config");
-    }
-
-    // ========== Token系数管理 ==========
-    @GetMapping({"/workflow/lobster/billing", "/workflow/lobster/billing/list"})
-    public TableDataInfo lobsterBilling() {
-        return safeListFromTable("lobster_billing_record");
-    }
-
-    // ========== 工作流执行引擎 lobster-exec ==========
-    @GetMapping({"/workflow/lobster-exec/instance", "/workflow/lobster-exec/instance/list"})
-    public TableDataInfo lobsterExecInstanceList() {
-        return safeListFromTable("workflow_instance");
-    }
-
-    @GetMapping("/workflow/lobster-exec/instance/{instanceId}")
-    public AjaxResult lobsterExecInstanceGet(@PathVariable String instanceId) {
-        return lobsterInstanceDetail(instanceId);
-    }
-
-    @GetMapping("/workflow/lobster-exec/node-logs/{instanceId}")
-    public AjaxResult lobsterExecNodeLogs(@PathVariable String instanceId) {
-        return lobsterInstanceNodeLogs(instanceId);
-    }
-
-    @PostMapping({"/workflow/lobster-exec/start", "/workflow/lobster-exec/next-node"})
-    public AjaxResult lobsterExecAction() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/pause/{instanceId}")
-    public AjaxResult lobsterExecPause(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/resume/{instanceId}")
-    public AjaxResult lobsterExecResume(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/terminate/{instanceId}")
-    public AjaxResult lobsterExecTerminate(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @GetMapping("/workflow/lobster-exec/control-mode/{instanceId}")
-    public AjaxResult lobsterExecGetControlMode(@PathVariable String instanceId) {
-        Map<String, Object> data = new HashMap<>();
-        data.put("instanceId", instanceId);
-        data.put("mode", "auto");
-        return AjaxResult.success(data);
-    }
-
-    @PostMapping("/workflow/lobster-exec/control-mode/{instanceId}")
-    public AjaxResult lobsterExecSetControlMode(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PostMapping("/workflow/lobster-exec/complete-handoff/{instanceId}")
-    public AjaxResult lobsterExecCompleteHandoff(@PathVariable String instanceId) {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== 合规规则 lobster-exec/compliance ==========
-    @GetMapping("/workflow/lobster-exec/compliance-rules")
-    public TableDataInfo lobsterExecComplianceRules() {
-        return emptyTable();
-    }
-
-    @PostMapping("/workflow/lobster-exec/compliance-rule")
-    public AjaxResult lobsterExecAddComplianceRule() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PutMapping("/workflow/lobster-exec/compliance-rule/{id}")
-    public AjaxResult lobsterExecUpdateComplianceRule(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @DeleteMapping("/workflow/lobster-exec/compliance-rule/{id}")
-    public AjaxResult lobsterExecDeleteComplianceRule(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== 引擎核心 lobster/engine ==========
-    @GetMapping("/workflow/lobster/engine/evolution/metrics")
-    public AjaxResult lobsterEngineEvolutionMetrics() {
-        Map<String, Object> data = new HashMap<>();
-        data.put("totalEvolutions", 0);
-        data.put("appliedCount", 0);
-        data.put("pendingCount", 0);
-        return AjaxResult.success(data);
-    }
-
-    @GetMapping("/workflow/lobster/engine/evolution/analyze")
-    public AjaxResult lobsterEngineEvolutionAnalyze() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    @PostMapping("/workflow/lobster/engine/evolution/apply")
-    public AjaxResult lobsterEngineEvolutionApply() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @GetMapping("/workflow/lobster/engine/heartbeat/status")
-    public AjaxResult lobsterEngineHeartbeat() {
-        Map<String, Object> data = new HashMap<>();
-        data.put("status", "healthy");
-        return AjaxResult.success(data);
-    }
-
-    @GetMapping("/workflow/lobster/engine/channels")
-    public AjaxResult lobsterEngineChannels() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    // ========== 提示词管理 CRUD ==========
-    @GetMapping("/workflow/lobster/prompt/{id}")
-    public AjaxResult lobsterPromptGet(@PathVariable Long id) {
-        return AjaxResult.success();
-    }
-
-    @PostMapping("/workflow/lobster/prompt")
-    public AjaxResult lobsterPromptAdd() {
-        return AjaxResult.success("操作成功");
-    }
-
-    @PutMapping("/workflow/lobster/prompt/{id}")
-    public AjaxResult lobsterPromptUpdate(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @DeleteMapping("/workflow/lobster/prompt/{id}")
-    public AjaxResult lobsterPromptDelete(@PathVariable Long id) {
-        return AjaxResult.success("操作成功");
-    }
-
-    @GetMapping("/workflow/lobster/prompt/categories")
-    public AjaxResult lobsterPromptCategories() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    @PostMapping("/workflow/lobster/prompt/refresh-cache")
-    public AjaxResult lobsterPromptRefreshCache() {
-        return AjaxResult.success("操作成功");
-    }
-
-    // ========== 销冠语料补充 ==========
-    @PostMapping("/workflow/lobster/sales-corpus/batch-import")
-    public AjaxResult lobsterSalesCorpusBatchImport(@RequestBody Map<String, Object> body) {
-        if (jdbcTemplate == null) return AjaxResult.error("数据库未初始化");
-        try {
-            Long companyId = body.get("companyId") != null ?
-                    Long.valueOf(body.get("companyId").toString()) : null;
-            String salespersonName = (String) body.getOrDefault("salespersonName", "销冠");
-            String scenario = (String) body.getOrDefault("scenario", "通用");
-            List<Map<String, Object>> dialogs = (List<Map<String, Object>>) body.get("dialogs");
-            if (dialogs == null || dialogs.isEmpty()) {
-                return AjaxResult.error("导入数据不能为空");
-            }
-            int count = 0;
-            for (Map<String, Object> dialog : dialogs) {
-                String customer = (String) dialog.get("customer");
-                String sales = (String) dialog.get("sales");
-                if (customer == null || customer.trim().isEmpty() || sales == null || sales.trim().isEmpty()) {
-                    continue;
-                }
-                jdbcTemplate.update(
-                    "INSERT INTO lobster_sales_corpus (company_id, salesperson_name, customer_question, sales_answer, scenario, status, create_time) " +
-                    "VALUES (?,?,?,?,?,'raw',NOW())",
-                    companyId, salespersonName, customer.trim(), sales.trim(), scenario);
-                count++;
-            }
-            Map<String, Object> result = new LinkedHashMap<>();
-            result.put("message", "批量导入完成");
-            result.put("count", count);
-            return AjaxResult.success(result);
-        } catch (Exception e) {
-            return AjaxResult.error("批量导入失败: " + e.getMessage());
-        }
-    }
-
-    // ========== 死信队列补充 ==========
-    @PostMapping("/workflow/lobster/dead-letter/retry-all")
-    public AjaxResult lobsterDeadLetterRetryAll() {
-        return AjaxResult.success("重试已提交");
-    }
-
-    @GetMapping("/workflow/lobster/dead-letter/stats")
-    public AjaxResult lobsterDeadLetterStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("total", 0);
-        stats.put("pending", 0);
-        stats.put("retried", 0);
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT status, COUNT(*) AS cnt FROM lobster_dead_letter GROUP BY status");
-                for (Map<String, Object> row : rows) {
-                    String s = String.valueOf(row.get("status"));
-                    long cnt = ((Number) row.get("cnt")).longValue();
-                    stats.put(s, cnt);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    // ========== 节点审核补充 ==========
-    @GetMapping("/workflow/lobster/event-audit/{id}")
-    public AjaxResult lobsterEventAuditGet(@PathVariable Long id) {
-        return AjaxResult.success();
-    }
-
-    @PostMapping("/workflow/lobster/event-audit/approve/{id}")
-    public AjaxResult lobsterEventAuditApprove(@PathVariable Long id) {
-        return AjaxResult.success("审批通过");
-    }
-
-    @PostMapping("/workflow/lobster/event-audit/reject/{id}")
-    public AjaxResult lobsterEventAuditReject(@PathVariable Long id) {
-        return AjaxResult.success("已驳回");
-    }
-
-    // ========== 优化建议补充 ==========
-    @GetMapping("/workflow/lobster/optimization/pending-audit")
-    public TableDataInfo lobsterOptimizationPendingAudit() {
-        return safeListFromTable("lobster_optimization_suggestion");
-    }
-
-    @PostMapping("/workflow/lobster/optimization/batch-audit")
-    public AjaxResult lobsterOptimizationBatchAudit() {
-        return AjaxResult.success("审核完成");
-    }
-
-    @PostMapping("/workflow/lobster/optimization/audit/{optimizationId}")
-    public AjaxResult lobsterOptimizationAuditSingle(@PathVariable Long optimizationId) {
-        return AjaxResult.success("审核完成");
-    }
-
-    @PostMapping("/workflow/lobster/optimization/analyze")
-    public AjaxResult lobsterOptimizationAnalyze() {
-        Map<String, Object> result = new HashMap<>();
-        result.put("totalSuggestions", 0);
-        return AjaxResult.success(result);
-    }
-
-    @GetMapping("/workflow/lobster/optimization/stats")
-    public AjaxResult lobsterOptimizationStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("total", 0);
-        stats.put("pending", 0);
-        stats.put("approved", 0);
-        stats.put("rejected", 0);
-        try {
-            if (jdbcTemplate != null) {
-                List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT status, COUNT(*) AS cnt FROM lobster_optimization_suggestion GROUP BY status");
-                for (Map<String, Object> row : rows) {
-                    String s = String.valueOf(row.get("status"));
-                    long cnt = ((Number) row.get("cnt")).longValue();
-                    stats.put(s, cnt);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    @GetMapping("/workflow/lobster/optimization/config")
-    public AjaxResult lobsterOptimizationConfig() {
-        return AjaxResult.success(new HashMap<>());
-    }
-
-    @PostMapping("/workflow/lobster/optimization/config")
-    public AjaxResult lobsterOptimizationSetConfig() {
-        return AjaxResult.success("配置已保存");
-    }
-
-    // ========== API注册中心补充 ==========
-    @PostMapping("/workflow/lobster/api-registry")
-    public AjaxResult lobsterApiRegistryAdd() {
-        return AjaxResult.success("注册成功");
-    }
-
-    @PostMapping("/workflow/lobster/api-registry/refresh")
-    public AjaxResult lobsterApiRegistryRefresh() {
-        return AjaxResult.success("缓存已刷新");
-    }
-
-    @GetMapping("/workflow/lobster/api-registry/categories")
-    public AjaxResult lobsterApiRegistryCategories() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-
-    // ========== Token计费补充 ==========
-    @GetMapping("/workflow/lobster/billing/token-coefficient")
-    public AjaxResult lobsterBillingTokenCoefficient() {
-        Map<String, Object> data = new HashMap<>();
-        data.put("coefficient", 1.0);
-        data.put("updatedAt", "");
-        return AjaxResult.success(data);
-    }
-
-    @PutMapping("/workflow/lobster/billing/token-coefficient")
-    public AjaxResult lobsterBillingUpdateTokenCoefficient() {
-        return AjaxResult.success("更新成功");
-    }
-
-    @GetMapping("/workflow/lobster/billing/records")
-    public TableDataInfo lobsterBillingRecords() {
-        return safeListFromTable("lobster_billing_record");
-    }
-
-    @GetMapping("/workflow/lobster/billing/stats")
-    public AjaxResult lobsterBillingStats() {
-        Map<String, Object> stats = new HashMap<>();
-        stats.put("totalTokens", 0);
-        stats.put("totalCost", 0);
-        try {
-            if (jdbcTemplate != null) {
-                Map<String, Object> row = jdbcTemplate.queryForMap(
-                    "SELECT COALESCE(SUM(token_count),0) AS totalTokens, COALESCE(SUM(cost),0) AS totalCost FROM lobster_billing_record");
-                if (row != null) {
-                    stats.putAll(row);
-                }
-            }
-        } catch (Exception ignored) {}
-        return AjaxResult.success(stats);
-    }
-
-    @GetMapping("/workflow/lobster/billing/types")
-    public AjaxResult lobsterBillingTypes() {
-        return AjaxResult.success(new ArrayList<>());
-    }
-}

+ 43 - 0
fs-agent/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql

@@ -0,0 +1,43 @@
+-- =====================================================
+-- 龙虾引擎 新增页面菜单
+-- 新增 7 个缺失的菜单记录(模拟聊天测试、评分准确性验证、
+--   用户画像配置、摘要生成配置、消息去重配置、敏感词库、节点详情)
+-- =====================================================
+
+-- =====================================================
+-- PART 1: 新增页面菜单 (menu_id 29920-29926)
+
+-- 15. 模拟聊天测试
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29920, '模拟聊天测试', 29900, 13, 'chat-test', 'lobster/chat-test/index', 'C', 'el-icon-chat-dot-square', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 16. 评分准确性验证
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29921, '评分准确性验证', 29900, 14, 'quality-verify', 'lobster/quality-verify/index', 'C', 'el-icon-data-analysis', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 17. 用户画像配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29922, '用户画像配置', 29900, 15, 'profile-config', 'lobster/profile-config/index', 'C', 'el-icon-user', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 18. 摘要生成配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29923, '摘要生成配置', 29900, 16, 'summary-config', 'lobster/summary-config/index', 'C', 'el-icon-notebook', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 19. 消息去重配置
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29924, '消息去重配置', 29900, 17, 'dedup-config', 'lobster/dedup-config/index', 'C', 'el-icon-filter', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 20. 敏感词库
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29925, '敏感词库', 29900, 18, 'sensitive-words', 'lobster/sensitive-words/index', 'C', 'el-icon-warning', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 21. 节点详情 (hidden, 从实例页跳转)
+INSERT IGNORE INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (29926, '节点详情', 29900, 19, 'node-detail', 'lobster/node-detail/index', 'C', 'el-icon-document', '1', '0', 0, 0, 'admin', NOW(), '从实例监控页跳转');
+
+-- =====================================================
+-- PART 2: 同步更新模板表 tenant_sys_menu
+-- =====================================================
+INSERT IGNORE INTO tenant_sys_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+SELECT menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark
+FROM sys_menu WHERE menu_id BETWEEN 29920 AND 29926;

+ 131 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java

@@ -0,0 +1,131 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.service.workflow.MultiModelWorkflowGenerator;
+import com.fs.company.service.workflow.MultiModelWorkflowGenerator.GenerationResult;
+import com.fs.company.service.workflow.MultiModelWorkflowGenerator.ModelConfig;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 龙虾引擎 AI 生成工作流 Controller
+ * 包装 MultiModelWorkflowGenerator,对接前端 /workflow/lobster/ai-generator/*
+ */
+@RestController
+@RequestMapping("/workflow/lobster/ai-generator")
+public class LobsterAiGeneratorController extends BaseController {
+
+    @Autowired
+    private MultiModelWorkflowGenerator generator;
+
+    /** 生成结果临时缓存:recordId → GenerationResult */
+    private final Map<Long, GenerationResult> resultCache = new ConcurrentHashMap<>();
+
+    /**
+     * 触发 AI 生成工作流
+     * 入参:requirement / industryType / companyId
+     */
+    @PostMapping("/generate")
+    public AjaxResult generate(@RequestBody Map<String, Object> body) {
+        Long companyId = body.get("companyId") != null ? Long.valueOf(body.get("companyId").toString()) : 0L;
+        String requirement = (String) body.getOrDefault("requirement", "");
+        String industryType = (String) body.getOrDefault("industryType", "general");
+
+        if (requirement.isEmpty()) {
+            return AjaxResult.error("requirement 不能为空");
+        }
+
+        // 模型配置走 admin_ai_scene 体系(workflow_generation 场景),传 null 即由 generator 内部按场景拉模型
+        ModelConfig modelConfig = null;
+        GenerationResult result = generator.generateWorkflowWithResult(companyId, requirement, industryType, modelConfig);
+
+        Long recordId = System.currentTimeMillis();
+        resultCache.put(recordId, result);
+
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("recordId", recordId);
+        resp.put("success", result.isSuccess());
+        resp.put("errorMsg", result.getErrorMsg());
+        resp.put("qualityScore", result.getQualityScore());
+        return AjaxResult.success(resp);
+    }
+
+    /**
+     * 查询生成结果详情
+     */
+    @GetMapping("/result/{recordId}/detail")
+    public AjaxResult getResult(@PathVariable Long recordId) {
+        GenerationResult result = resultCache.get(recordId);
+        if (result == null) {
+            return AjaxResult.error("生成结果不存在或已过期");
+        }
+        Map<String, Object> data = new HashMap<>();
+        data.put("recordId", recordId);
+        data.put("success", result.isSuccess());
+        data.put("workflowJson", result.getWorkflowJson());
+        data.put("modelAOutput", result.getModelAOutput());
+        data.put("modelBOutput", result.getModelBOutput());
+        data.put("modelCOutput", result.getModelCOutput());
+        data.put("qualityScore", result.getQualityScore());
+        data.put("suggestions", result.getSuggestions());
+        data.put("errorMsg", result.getErrorMsg());
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 确认生成结果(保存为工作流模板)
+     * 入参:workflowName / companyId
+     */
+    @PostMapping("/confirm/{recordId}")
+    public AjaxResult confirm(@PathVariable Long recordId, @RequestBody Map<String, Object> body) {
+        GenerationResult result = resultCache.get(recordId);
+        if (result == null || !result.isSuccess()) {
+            return AjaxResult.error("生成结果不存在或失败");
+        }
+        // TODO:调 CompanyWorkflowLobsterService.saveAsTemplate(json, workflowName, companyId)
+        // 当前先返回 JSON 给前端,由前端走原有保存流程
+        Map<String, Object> data = new HashMap<>();
+        data.put("workflowJson", result.getWorkflowJson());
+        data.put("workflowName", body.get("workflowName"));
+        resultCache.remove(recordId);
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 确认(带前端编辑后内容)
+     */
+    @PostMapping("/confirm/{recordId}/edited")
+    public AjaxResult confirmEdited(@PathVariable Long recordId, @RequestBody Map<String, Object> body) {
+        String editedJson = (String) body.get("workflowJson");
+        Map<String, Object> data = new HashMap<>();
+        data.put("workflowJson", editedJson);
+        data.put("workflowName", body.get("workflowName"));
+        resultCache.remove(recordId);
+        return AjaxResult.success(data);
+    }
+
+    /**
+     * 基于现有工作流再次生成(迭代优化)
+     */
+    @PostMapping("/regenerate/{recordId}")
+    public AjaxResult regenerate(@PathVariable Long recordId, @RequestBody Map<String, Object> body) {
+        Long companyId = body.get("companyId") != null ? Long.valueOf(body.get("companyId").toString()) : 0L;
+        String existingJson = (String) body.getOrDefault("existingWorkflowJson", "");
+        String modifyInstruction = (String) body.getOrDefault("modifyInstruction", "");
+
+        GenerationResult result = generator.iterateOptimize(companyId, existingJson, modifyInstruction, null);
+        Long newRecordId = System.currentTimeMillis();
+        resultCache.put(newRecordId, result);
+
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("recordId", newRecordId);
+        resp.put("success", result.isSuccess());
+        resp.put("errorMsg", result.getErrorMsg());
+        return AjaxResult.success(resp);
+    }
+}

+ 10 - 48
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterBillingController.java

@@ -3,11 +3,11 @@ package com.fs.company.controller.workflow;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.ServletUtils;
-import com.fs.company.service.billing.BillingService;
+import com.fs.company.domain.LobsterTenantBalance;
+import com.fs.company.service.workflow.ILobsterBillingService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
@@ -23,11 +23,8 @@ import java.util.*;
 @RequestMapping("/workflow/lobster/billing")
 public class LobsterBillingController extends BaseController {
 
-    @Autowired(required = false)
-    private BillingService billingService;
-
-    @Autowired(required = false)
-    private JdbcTemplate jdbcTemplate;
+    @Autowired
+    private ILobsterBillingService billingService;
 
     @Autowired
     private TokenService tokenService;
@@ -39,8 +36,9 @@ public class LobsterBillingController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long tenantId = loginUser.getCompany().getCompanyId();
 
-        BigDecimal coef = billingService != null ?
-                billingService.getTokenCoefficient(tenantId) : new BigDecimal("1.00");
+        LobsterTenantBalance balance = billingService.getBalance(tenantId);
+        BigDecimal coef = (balance != null && balance.getTokenCoefficient() != null) ?
+                balance.getTokenCoefficient() : new BigDecimal("1.00");
 
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("tenantId", tenantId);
@@ -70,23 +68,8 @@ public class LobsterBillingController extends BaseController {
             return AjaxResult.error("系数范围: 0.01 ~ 100");
         }
 
-        if (billingService != null) {
-            boolean ok = billingService.updateTokenCoefficient(tenantId, coefficient);
-            if (ok) {
-                try {
-                    jdbcTemplate.update("UPDATE tenant_balance SET token_coefficient=? WHERE tenant_id=?", coefficient, tenantId);
-                } catch (Exception ignored) {}
-                return AjaxResult.success("Token系数已更新为 " + coefficient);
-            }
-            return AjaxResult.error("更新失败");
-        }
-
-        try {
-            jdbcTemplate.update("UPDATE tenant_balance SET token_coefficient=? WHERE tenant_id=?", coefficient, tenantId);
-            return AjaxResult.success("Token系数已更新为 " + coefficient);
-        } catch (Exception e) {
-            return AjaxResult.error("更新失败: " + e.getMessage());
-        }
+        boolean ok = billingService.updateTokenCoefficient(tenantId, coefficient);
+        return ok ? AjaxResult.success("Token系数已更新为 " + coefficient) : AjaxResult.error("更新失败");
     }
 
     /** 消费记录列表 */
@@ -96,29 +79,8 @@ public class LobsterBillingController extends BaseController {
                               @RequestParam(defaultValue = "10") int size) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long tenantId = loginUser.getCompany().getCompanyId();
-        List<Map<String, Object>> list;
-        try {
-            list = jdbcTemplate.queryForList(
-                    "SELECT * FROM tenant_consume_record WHERE tenant_id=? ORDER BY consume_time DESC LIMIT ? OFFSET ?",
-                    tenantId, size, (page - 1) * size);
-        } catch (Exception e) {
-            list = jdbcTemplate.queryForList(
-                    "SELECT id, tenant_id, consume_type, amount, remark, status, consume_time FROM tenant_consume_record WHERE tenant_id=? ORDER BY consume_time DESC LIMIT ? OFFSET ?",
-                    tenantId, size, (page - 1) * size);
-        }
-
-        Long total;
-        try {
-            total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM tenant_consume_record WHERE tenant_id=?", Long.class, tenantId);
-        } catch (Exception e) {
-            total = 0L;
-        }
 
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("list", list);
-        result.put("total", total);
-        result.put("page", page);
-        result.put("size", size);
+        Map<String, Object> result = billingService.listConsumeRecords(page, size, tenantId);
         return AjaxResult.success(result);
     }
 

+ 13 - 41
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEventAuditController.java

@@ -4,11 +4,12 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.LobsterEventAudit;
+import com.fs.company.service.workflow.ILobsterEventAuditService;
 import com.fs.company.service.workflow.event.WorkflowPatcher;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
@@ -27,7 +28,7 @@ import java.util.*;
 public class LobsterEventAuditController extends BaseController {
 
     @Autowired
-    private JdbcTemplate jdbcTemplate;
+    private ILobsterEventAuditService auditService;
 
     @Autowired
     private TokenService tokenService;
@@ -35,8 +36,6 @@ public class LobsterEventAuditController extends BaseController {
     @Autowired(required = false)
     private WorkflowPatcher workflowPatcher;
 
-    private static final String TABLE = "lobster_event_node_audit";
-
     /**
      * 待审列表
      */
@@ -48,28 +47,7 @@ public class LobsterEventAuditController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
 
-        List<Map<String, Object>> list = jdbcTemplate.queryForList(
-                "SELECT * FROM " + TABLE + " WHERE company_id=? AND status=? ORDER BY create_time DESC LIMIT ? OFFSET ?",
-                companyId, status, size, (page - 1) * size);
-
-        Long total = jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM " + TABLE + " WHERE company_id=? AND status=?", Long.class, companyId, status);
-
-        // 统计各状态数量
-        Map<String, Object> stats = new LinkedHashMap<>();
-        stats.put("pending", jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM " + TABLE + " WHERE company_id=? AND status='pending'", Long.class, companyId));
-        stats.put("approved", jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM " + TABLE + " WHERE company_id=? AND status='approved'", Long.class, companyId));
-        stats.put("rejected", jdbcTemplate.queryForObject(
-                "SELECT COUNT(*) FROM " + TABLE + " WHERE company_id=? AND status='rejected'", Long.class, companyId));
-
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("list", list);
-        result.put("total", total);
-        result.put("page", page);
-        result.put("size", size);
-        result.put("stats", stats);
+        Map<String, Object> result = auditService.listAudits(status, page, size, companyId);
         return AjaxResult.success(result);
     }
 
@@ -81,15 +59,14 @@ public class LobsterEventAuditController extends BaseController {
     public AjaxResult approve(@PathVariable Long id) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
-        Map<String, Object> record = jdbcTemplate.queryForMap(
-                "SELECT * FROM " + TABLE + " WHERE id=? AND status='pending'", id);
-        if (record == null || record.isEmpty()) {
+        LobsterEventAudit record = auditService.getById(id);
+        if (record == null || !"pending".equals(record.getStatus())) {
             return AjaxResult.error("审核记录不存在或已处理");
         }
 
-        Long instanceId = record.get("instance_id") != null ? ((Number) record.get("instance_id")).longValue() : null;
-        String nodeJson = (String) record.get("node_json");
-        String insertAt = (String) record.get("insert_at");
+        Long instanceId = record.getInstanceId();
+        String nodeJson = record.getNodeJson();
+        String insertAt = record.getInsertAt();
 
         boolean patched = false;
         if (instanceId != null && nodeJson != null && workflowPatcher != null) {
@@ -101,9 +78,7 @@ public class LobsterEventAuditController extends BaseController {
             }
         }
 
-        jdbcTemplate.update(
-                "UPDATE " + TABLE + " SET status='approved', audit_by=?, audit_comment=?, audit_time=NOW(), update_time=NOW() WHERE id=?",
-                loginUser.getUsername(), patched ? "审核通过,节点已注入" : "审核通过", id);
+        auditService.approve(id, loginUser.getUsername());
 
         Map<String, Object> result = new HashMap<>();
         result.put("patched", patched);
@@ -120,12 +95,9 @@ public class LobsterEventAuditController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
         String comment = body != null ? (String) body.getOrDefault("comment", "人工驳回") : "人工驳回";
+        boolean ok = auditService.reject(id, loginUser.getUsername(), comment);
 
-        int rows = jdbcTemplate.update(
-                "UPDATE " + TABLE + " SET status='rejected', audit_by=?, audit_comment=?, audit_time=NOW(), update_time=NOW() WHERE id=?",
-                loginUser.getUsername(), comment, id);
-
-        return rows > 0 ? AjaxResult.success("已驳回") : AjaxResult.error("审核记录不存在");
+        return ok ? AjaxResult.success("已驳回") : AjaxResult.error("审核记录不存在");
     }
 
     /**
@@ -134,7 +106,7 @@ public class LobsterEventAuditController extends BaseController {
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/{id}")
     public AjaxResult detail(@PathVariable Long id) {
-        Map<String, Object> record = jdbcTemplate.queryForMap("SELECT * FROM " + TABLE + " WHERE id=?", id);
+        LobsterEventAudit record = auditService.getById(id);
         return AjaxResult.success(record);
     }
 }

+ 108 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterModelRouteController.java

@@ -0,0 +1,108 @@
+package com.fs.company.controller.workflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.LobsterModelConfig;
+import com.fs.company.service.workflow.LobsterModelConfigService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+/**
+ * 龙虾多模型路由配置Controller(租户端,非桥接)
+ * 管理 lobster_model_config 表的CRUD
+ */
+@RestController
+@RequestMapping("/workflow/lobster/model-route")
+public class LobsterModelRouteController extends BaseController {
+
+    @Autowired
+    private LobsterModelConfigService modelConfigService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    private Long getCurrentCompanyId() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return loginUser.getCompany().getCompanyId();
+    }
+
+    /**
+     * 获取当前租户的所有模型路由配置
+     */
+    @GetMapping("/list")
+    public AjaxResult list() {
+        Long companyId = getCurrentCompanyId();
+        List<LobsterModelConfig> configs = modelConfigService.getConfigsByCompany(companyId);
+        return AjaxResult.success(configs);
+    }
+
+    /**
+     * 根据配置类型获取配置
+     */
+    @GetMapping("/{configType}")
+    public AjaxResult getByType(@PathVariable String configType) {
+        Long companyId = getCurrentCompanyId();
+        LobsterModelConfig config = modelConfigService.getConfig(companyId, configType);
+        return AjaxResult.success(config);
+    }
+
+    /**
+     * 新增模型路由配置
+     */
+    @PostMapping
+    public AjaxResult add(@RequestBody LobsterModelConfig config) {
+        Long companyId = getCurrentCompanyId();
+        config.setCompanyId(companyId);
+        config.setEnabled(1);
+        config.setCreatedAt(LocalDateTime.now());
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.saveConfig(config);
+        return AjaxResult.success("配置已保存");
+    }
+
+    /**
+     * 更新模型路由配置
+     */
+    @PutMapping("/{id}")
+    public AjaxResult update(@PathVariable Long id, @RequestBody LobsterModelConfig config) {
+        config.setId(id);
+        config.setUpdatedAt(LocalDateTime.now());
+        modelConfigService.updateConfig(config);
+        return AjaxResult.success("配置已更新");
+    }
+
+    /**
+     * 删除模型路由配置
+     */
+    @DeleteMapping("/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        modelConfigService.deleteConfig(id);
+        return AjaxResult.success("配置已删除");
+    }
+
+    /**
+     * 获取支持的配置类型列表
+     */
+    @GetMapping("/types")
+    public AjaxResult types() {
+        List<Map<String, String>> types = new ArrayList<>();
+        types.add(buildType("workflow_generator", "工作流生成"));
+        types.add(buildType("quality_check", "质检评分"));
+        types.add(buildType("intent_router", "意图路由"));
+        types.add(buildType("reply_generator", "回复生成"));
+        return AjaxResult.success(types);
+    }
+
+    private Map<String, String> buildType(String code, String name) {
+        Map<String, String> m = new LinkedHashMap<>();
+        m.put("code", code);
+        m.put("name", name);
+        return m;
+    }
+}

+ 29 - 66
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPromptController.java

@@ -3,11 +3,11 @@ package com.fs.company.controller.workflow;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.ServletUtils;
-import com.fs.company.service.workflow.prompt.SystemPromptService;
+import com.fs.company.domain.LobsterSystemPrompt;
+import com.fs.company.service.workflow.ILobsterPromptService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
@@ -23,54 +23,25 @@ import java.util.*;
 public class LobsterPromptController extends BaseController {
 
     @Autowired
-    private JdbcTemplate jdbcTemplate;
+    private ILobsterPromptService promptService;
 
     @Autowired
     private TokenService tokenService;
 
-    @Autowired(required = false)
-    private SystemPromptService promptService;
-
-    private static final String TABLE = "lobster_system_prompt";
-
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/list")
     public AjaxResult list(@RequestParam(defaultValue = "1") int page,
                            @RequestParam(defaultValue = "10") int size,
                            @RequestParam(required = false) String category,
                            @RequestParam(required = false) String search) {
-        StringBuilder where = new StringBuilder(" WHERE enabled=1 ");
-        List<Object> params = new ArrayList<>();
-        if (category != null && !category.isEmpty()) {
-            where.append("AND prompt_category=? ");
-            params.add(category);
-        }
-        if (search != null && !search.isEmpty()) {
-            where.append("AND (prompt_name LIKE ? OR prompt_key LIKE ?) ");
-            params.add("%" + search + "%");
-            params.add("%" + search + "%");
-        }
-        List<Object> countParams = new ArrayList<>(params);
-        // 添加 LIMIT / OFFSET 参数
-        params.add(size);
-        params.add((page - 1) * size);
-        List<Map<String, Object>> list = jdbcTemplate.queryForList(
-                "SELECT * FROM " + TABLE + where + "ORDER BY company_id, industry_type, sort_order LIMIT ? OFFSET ?",
-                params.toArray());
-        Long total = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + TABLE + where, Long.class, countParams.toArray());
-
-        Map<String, Object> result = new HashMap<>();
-        result.put("list", list);
-        result.put("total", total);
-        result.put("page", page);
-        result.put("size", size);
+        Map<String, Object> result = promptService.listPrompts(page, size, category, search);
         return AjaxResult.success(result);
     }
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/{id}")
     public AjaxResult getById(@PathVariable Long id) {
-        Map<String, Object> row = jdbcTemplate.queryForMap("SELECT * FROM " + TABLE + " WHERE id=?", id);
+        LobsterSystemPrompt row = promptService.getById(id);
         return AjaxResult.success(row);
     }
 
@@ -78,22 +49,19 @@ public class LobsterPromptController extends BaseController {
     @PostMapping
     public AjaxResult create(@RequestBody Map<String, Object> body) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        String key = (String) body.getOrDefault("promptKey", "custom_" + System.currentTimeMillis());
-        String name = (String) body.getOrDefault("promptName", "自定义提示词");
-        String category = (String) body.getOrDefault("promptCategory", "custom");
         String content = (String) body.get("promptContent");
         if (content == null || content.isEmpty()) return AjaxResult.error("提示词内容不能为空");
-        jdbcTemplate.update(
-                "INSERT INTO " + TABLE + " (prompt_key,prompt_name,prompt_category,prompt_content,model_name," +
-                "system_role,company_id,industry_type,enabled,sort_order,create_by,create_time) " +
-                "VALUES (?,?,?,?,?,?,?,?,1,0,?,NOW())",
-                key, name, category, content,
-                body.getOrDefault("modelName", "doubao-lite"),
-                body.get("systemRole"),
-                loginUser.getCompany().getCompanyId(),
-                body.get("industryType"),
-                loginUser.getUsername());
-        if (promptService != null) promptService.refreshCache();
+
+        LobsterSystemPrompt prompt = new LobsterSystemPrompt();
+        prompt.setPromptKey((String) body.getOrDefault("promptKey", "custom_" + System.currentTimeMillis()));
+        prompt.setPromptName((String) body.getOrDefault("promptName", "自定义提示词"));
+        prompt.setPromptCategory((String) body.getOrDefault("promptCategory", "custom"));
+        prompt.setPromptContent(content);
+        prompt.setModelName((String) body.getOrDefault("modelName", "doubao-lite"));
+        prompt.setSystemRole((String) body.get("systemRole"));
+        prompt.setIndustryType((String) body.get("industryType"));
+
+        promptService.create(prompt, loginUser.getUsername(), loginUser.getCompany().getCompanyId());
         return AjaxResult.success("创建成功");
     }
 
@@ -101,41 +69,36 @@ public class LobsterPromptController extends BaseController {
     @PutMapping("/{id}")
     public AjaxResult update(@PathVariable Long id, @RequestBody Map<String, Object> body) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        jdbcTemplate.update(
-                "UPDATE " + TABLE + " SET prompt_name=?, prompt_category=?, prompt_content=?, " +
-                "model_name=?, system_role=?, industry_type=?, update_time=NOW() WHERE id=? AND company_id=?",
-                body.getOrDefault("promptName", ""),
-                body.getOrDefault("promptCategory", ""),
-                body.get("promptContent"),
-                body.getOrDefault("modelName", "doubao-lite"),
-                body.get("systemRole"),
-                body.get("industryType"),
-                id, loginUser.getCompany().getCompanyId());
-        if (promptService != null) promptService.refreshCache();
+
+        LobsterSystemPrompt prompt = new LobsterSystemPrompt();
+        prompt.setPromptName((String) body.getOrDefault("promptName", ""));
+        prompt.setPromptCategory((String) body.getOrDefault("promptCategory", ""));
+        prompt.setPromptContent((String) body.get("promptContent"));
+        prompt.setModelName((String) body.getOrDefault("modelName", "doubao-lite"));
+        prompt.setSystemRole((String) body.get("systemRole"));
+        prompt.setIndustryType((String) body.get("industryType"));
+
+        promptService.update(id, prompt, loginUser.getCompany().getCompanyId());
         return AjaxResult.success("更新成功");
     }
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @DeleteMapping("/{id}")
     public AjaxResult delete(@PathVariable Long id) {
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        jdbcTemplate.update("UPDATE " + TABLE + " SET enabled=0, update_time=NOW() WHERE id=? AND company_id=?",
-                id, loginUser.getCompany().getCompanyId());
-        if (promptService != null) promptService.refreshCache();
+        promptService.softDelete(id);
         return AjaxResult.success("删除成功");
     }
 
     @GetMapping("/categories")
     public AjaxResult categories() {
-        List<String> cats = jdbcTemplate.queryForList(
-                "SELECT DISTINCT prompt_category FROM " + TABLE + " WHERE enabled=1", String.class);
+        List<String> cats = promptService.getCategories();
         return AjaxResult.success(cats);
     }
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PostMapping("/refresh-cache")
     public AjaxResult refreshCache() {
-        if (promptService != null) promptService.refreshCache();
+        promptService.refreshCache();
         return AjaxResult.success("缓存已刷新");
     }
 }

+ 5 - 27
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterSalesCorpusController.java

@@ -4,13 +4,13 @@ import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.utils.ServletUtils;
+import com.fs.company.service.workflow.ILobsterSalesCorpusService;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer.AnalysisReport;
 import com.fs.company.service.workflow.learning.SalesCorpusAnalyzer.CorpusEntry;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
@@ -19,7 +19,7 @@ import java.util.*;
 /**
  * 龙虾销冠语料管理Controller
  *
- * 表: lobster_learning_corpus
+ * 表: lobster_sales_corpus
  * 页面: 销冠语料 → 录入/批量导入/AI分析/话术库查询
  *
  * 核心价值: 租户上传销冠/金牌客服聊天话术 → AI分析提取沟通模式 → 进化引擎学习 → 全租户共享
@@ -31,14 +31,12 @@ public class LobsterSalesCorpusController extends BaseController {
     @Autowired(required = false)
     private SalesCorpusAnalyzer corpusAnalyzer;
 
-    @Autowired(required = false)
-    private JdbcTemplate jdbcTemplate;
+    @Autowired
+    private ILobsterSalesCorpusService salesCorpusService;
 
     @Autowired
     private TokenService tokenService;
 
-    private static final String TABLE = "lobster_learning_corpus";
-
     /**
      * 单条录入销冠对话
      */
@@ -157,27 +155,7 @@ public class LobsterSalesCorpusController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
 
-        StringBuilder where = new StringBuilder("WHERE company_id=? ");
-        List<Object> params = new ArrayList<>();
-        params.add(companyId);
-        if (scenario != null && !scenario.isEmpty()) {
-            where.append("AND scenario=? ");
-            params.add(scenario);
-        }
-        if (status != null && !status.isEmpty()) {
-            where.append("AND status=? ");
-            params.add(status);
-        }
-
-        List<Map<String, Object>> list = jdbcTemplate != null ?
-                jdbcTemplate.queryForList("SELECT * FROM " + TABLE + " " + where +
-                        "ORDER BY create_time DESC LIMIT ? OFFSET ?",
-                        params.toArray(new Object[0]).clone()) : Collections.emptyList();
-
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("list", list);
-        result.put("page", page);
-        result.put("size", size);
+        Map<String, Object> result = salesCorpusService.listCorpus(page, size, companyId, scenario, status);
         return AjaxResult.success(result);
     }
 

+ 21 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java

@@ -247,4 +247,25 @@ public class LobsterWorkflowExecController extends BaseController {
         result.put("controlUpdatedAt", instance.getControlUpdatedAt());
         return AjaxResult.success(result);
     }
+
+    /**
+     * 模拟聊天测试:使用指定模板进行对话模拟
+     */
+    @PreAuthorize("@ss.hasPermi('workflow:lobster:exec')")
+    @PostMapping("/simulate")
+    public AjaxResult simulateChat(@RequestBody Map<String, Object> params) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        String content = (String) params.getOrDefault("content", "");
+        Long templateId = params.get("templateId") != null ? Long.valueOf(params.get("templateId").toString()) : null;
+
+        // 模拟对话响应(后续可对接真实的 LobsterWorkflowExecutor 模拟执行能力)
+        Map<String, Object> reply = new java.util.HashMap<>();
+        reply.put("reply", "[模拟回复] \u300c" + content + "\u300d — 模板ID:" + (templateId != null ? templateId : "N/A"));
+        reply.put("templateId", templateId);
+        reply.put("companyId", companyId);
+        reply.put("timestamp", System.currentTimeMillis());
+        reply.put("mode", "simulate");
+        return AjaxResult.success(reply);
+    }
 }

+ 176 - 0
fs-service/src/main/java/com/fs/company/controller/LobsterAdminController.java

@@ -0,0 +1,176 @@
+package com.fs.company.controller;
+
+import com.fs.company.service.workflow.evolution.EvolutionEngine;
+import com.fs.company.service.workflow.evolution.UserNodeOptimizer;
+import com.fs.company.service.workflow.evolution.impl.EvolutionSchedulerImpl;
+import com.fs.company.service.workflow.feedback.FeedbackDrivenEvolution;
+import com.fs.company.service.workflow.learning.TenantLearningEngine;
+import com.fs.company.service.workflow.monitor.DashboardService;
+import com.fs.company.service.workflow.pay.PayService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 龙虾引擎 Admin REST API
+ * <p>
+ * 提供前端调用入口:进化审批 / 学习结果 / A/B测试 / 监控看板 / 支付查询
+ */
+@RestController
+@RequestMapping("/api/lobster/admin")
+public class LobsterAdminController {
+
+    private static final Logger log = LoggerFactory.getLogger(LobsterAdminController.class);
+
+    @Autowired(required = false)
+    private EvolutionEngine evolutionEngine;
+
+    @Autowired(required = false)
+    private UserNodeOptimizer userNodeOptimizer;
+
+    @Autowired(required = false)
+    private FeedbackDrivenEvolution feedbackDrivenEvolution;
+
+    @Autowired(required = false)
+    private TenantLearningEngine tenantLearningEngine;
+
+    @Autowired(required = false)
+    private EvolutionSchedulerImpl evolutionScheduler;
+
+    @Autowired(required = false)
+    private DashboardService dashboardService;
+
+    @Autowired(required = false)
+    private PayService payService;
+
+    // ════════════ 进化管理 ════════════
+
+    /** 租户进化指标统计 */
+    @GetMapping("/evolution/metrics/{companyId}")
+    public Map<String, Object> evolutionMetrics(@PathVariable Long companyId) {
+        return evolutionEngine != null ? evolutionEngine.getEvolutionMetrics(companyId) : Collections.emptyMap();
+    }
+
+    /** 应用优化建议 */
+    @PostMapping("/evolution/apply/{companyId}/{suggestionId}")
+    public Map<String, Object> applySuggestion(@PathVariable Long companyId, @PathVariable Long suggestionId) {
+        evolutionEngine.applySuggestion(companyId, suggestionId);
+        return Map.of("result", "applied");
+    }
+
+    // ════════════ 用户级优化 ════════════
+
+    /** 待审核优化列表 */
+    @GetMapping("/optimization/pending/{companyId}")
+    public Map<String, Object> pendingOptimizations(@PathVariable Long companyId,
+                                                     @RequestParam(defaultValue = "1") int page,
+                                                     @RequestParam(defaultValue = "20") int pageSize,
+                                                     @RequestParam(required = false) Long workflowId) {
+        return userNodeOptimizer != null
+            ? Map.of("list", userNodeOptimizer.getPendingAuditList(companyId, workflowId, page, pageSize))
+            : Collections.emptyMap();
+    }
+
+    /** 批量审核优化 */
+    @PostMapping("/optimization/audit/{companyId}")
+    public Map<String, Object> batchAudit(@PathVariable Long companyId,
+                                           @RequestParam String auditorId,
+                                           @RequestBody List<Map<String, Object>> items) {
+        List<UserNodeOptimizer.AuditItem> auditItems = new ArrayList<>();
+        for (Map<String, Object> item : items) {
+            UserNodeOptimizer.AuditItem ai = new UserNodeOptimizer.AuditItem();
+            ai.setOptimizationId(((Number) item.get("id")).longValue());
+            ai.setApproved(Boolean.TRUE.equals(item.get("approved")));
+            auditItems.add(ai);
+        }
+        return Map.of("result", userNodeOptimizer != null
+            ? userNodeOptimizer.batchAudit(companyId, auditItems, auditorId, "").getSummary()
+            : "optimizer unavailable");
+    }
+
+    // ════════════ 用户级优化 ════════════
+
+    /** 优化开关配置 */
+    @PostMapping("/optimization/config/{companyId}")
+    public Map<String, Object> setOptimizationConfig(@PathVariable Long companyId,
+                                                      @RequestParam Long workflowId,
+                                                      @RequestParam String nodeCode,
+                                                      @RequestParam boolean enabled,
+                                                      @RequestParam boolean autoApply,
+                                                      @RequestParam(defaultValue = "admin") String configBy) {
+        userNodeOptimizer.setOptimizationConfig(companyId, workflowId, nodeCode, enabled, autoApply, configBy);
+        return Map.of("result", "ok");
+    }
+
+    // ════════════ A/B 测试 ════════════
+
+    /** 活跃 A/B 测试列表 */
+    @GetMapping("/ab-test/active/{companyId}")
+    public Map<String, Object> activeAbTests(@PathVariable Long companyId) {
+        return feedbackDrivenEvolution != null
+            ? Map.of("tests", feedbackDrivenEvolution.getActiveABTests(companyId))
+            : Collections.emptyMap();
+    }
+
+    /** 应用 A/B 测试结果 */
+    @PostMapping("/ab-test/apply/{companyId}/{testId}")
+    public Map<String, Object> applyAbTest(@PathVariable Long companyId, @PathVariable Long testId) {
+        return feedbackDrivenEvolution != null
+            ? Map.of("result", feedbackDrivenEvolution.applyABTestResult(companyId, testId).getMessage())
+            : Map.of("result", "unavailable");
+    }
+
+    // ════════════ 学习引擎 ════════════
+
+    /** 触发学习周期 */
+    @PostMapping("/learning/trigger/{companyId}")
+    public Map<String, Object> triggerLearning(@PathVariable Long companyId) {
+        return tenantLearningEngine != null
+            ? Map.of("summary", tenantLearningEngine.triggerLearningCycle(companyId).getSummary())
+            : Map.of("result", "unavailable");
+    }
+
+    /** 学习结果列表 */
+    @GetMapping("/learning/results/{companyId}")
+    public Map<String, Object> learningResults(@PathVariable Long companyId) {
+        return tenantLearningEngine != null
+            ? Map.of("results", tenantLearningEngine.getLearningResults(companyId))
+            : Collections.emptyMap();
+    }
+
+    /** 应用学习结果 */
+    @PostMapping("/learning/apply/{companyId}/{resultId}")
+    public Map<String, Object> applyLearning(@PathVariable Long companyId, @PathVariable Long resultId) {
+        return tenantLearningEngine != null
+            ? Map.of("result", tenantLearningEngine.applyLearningResult(companyId, resultId, null).getMessage())
+            : Map.of("result", "unavailable");
+    }
+
+    // ════════════ 监控看板 ════════════
+
+    /** 租户全景看板 */
+    @GetMapping("/dashboard/{companyId}")
+    public Map<String, Object> dashboard(@PathVariable Long companyId) {
+        return dashboardService != null ? dashboardService.getTenantDashboard(companyId) : Collections.emptyMap();
+    }
+
+    // ════════════ 支付 ════════════
+
+    /** 支付状态查询 */
+    @GetMapping("/pay/status")
+    public Map<String, Object> payStatus(@RequestParam String orderNo, @RequestParam(defaultValue = "wechat") String gateway) {
+        return payService != null ? payService.checkPayStatus(orderNo, gateway) : Collections.emptyMap();
+    }
+
+    // ════════════ 测试结果 ════════════
+
+    /** 手动触发进化调度 */
+    @PostMapping("/scheduler/trigger")
+    public Map<String, Object> triggerScheduler() {
+        evolutionScheduler.recordTask(0L, "manual_trigger", "ok");
+        return Map.of("result", "triggered");
+    }
+}

+ 71 - 0
fs-service/src/main/java/com/fs/company/controller/PayCallbackController.java

@@ -0,0 +1,71 @@
+package com.fs.company.controller;
+
+import com.fs.company.service.workflow.pay.PayService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.*;
+
+/**
+ * 支付回调 Webhook 接收端点
+ * <p>
+ * 接收微信/支付宝/银联的异步支付通知,自动推进工作流
+ */
+@RestController
+@RequestMapping("/api/lobster/pay")
+public class PayCallbackController {
+
+    private static final Logger log = LoggerFactory.getLogger(PayCallbackController.class);
+
+    @Autowired(required = false)
+    private PayService payService;
+
+    /**
+     * 微信支付异步通知
+     */
+    @PostMapping("/notify/wechat")
+    public Map<String, Object> wechatNotify(@RequestBody String body) {
+        log.info("[PayCallback] 微信支付通知: {}", body);
+        try {
+            String orderNo = extractField(body, "out_trade_no");
+            String transactionId = extractField(body, "transaction_id");
+            if (orderNo != null && !orderNo.isEmpty()) {
+                if (payService != null) payService.checkPayStatus(orderNo, "wechat");
+            }
+            return Map.of("code", "SUCCESS", "message", "OK");
+        } catch (Exception e) {
+            return Map.of("code", "FAIL", "message", e.getMessage());
+        }
+    }
+
+    /**
+     * 支付宝异步通知
+     */
+    @PostMapping("/notify/alipay")
+    public Map<String, Object> alipayNotify(@RequestParam Map<String, String> params) {
+        log.info("[PayCallback] 支付宝支付通知: {}", params);
+        String orderNo = params.get("out_trade_no");
+        if (orderNo != null && payService != null) {
+            payService.checkPayStatus(orderNo, "alipay");
+        }
+        return Map.of("code", "SUCCESS", "message", "OK");
+    }
+
+    private String extractField(String xmlOrJson, String key) {
+        int idx = xmlOrJson.indexOf("\"" + key + "\"");
+        if (idx < 0) {
+            idx = xmlOrJson.indexOf("<" + key + ">");
+            if (idx >= 0) {
+                int start = idx + key.length() + 2;
+                int end = xmlOrJson.indexOf("</" + key + ">", start);
+                return end > start ? xmlOrJson.substring(start, end) : "";
+            }
+            return "";
+        }
+        int start = xmlOrJson.indexOf("\"", idx + key.length() + 3);
+        int end = xmlOrJson.indexOf("\"", start + 1);
+        return start > 0 && end > start ? xmlOrJson.substring(start + 1, end) : "";
+    }
+}

+ 6 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterNode.java

@@ -38,6 +38,12 @@ public class CompanyWorkflowLobsterNode extends BaseEntity {
 
     private String nodeConfig;
 
+    /** AI 场景编码(对应 admin_ai_scene.scene_code),节点执行时按场景挑模型 */
+    private String sceneCode;
+
+    /** 指定具体模型(对应 admin_ai_model.model_name),优先级高于 sceneCode */
+    private String modelName;
+
     private String greetingConfig;
 
     private LocalTime sendTime;

+ 29 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterChannelPluginConfig.java

@@ -0,0 +1,29 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾渠道插件配置 Domain
+ * <p>
+ * 表: lobster_channel_plugin_config
+ * 即插即用多IM通道:每个租户可为每个渠道(QW/WX/WHATSAPP/LINE/...)独立配置启用状态和API凭证
+ */
+@Data
+public class LobsterChannelPluginConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 渠道类型: QW/WX/IM/WHATSAPP/LINE/TELEGRAM/APP_IM/OTHER */
+    private String channelType;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 渠道配置 JSON (API Key / Webhook URL / Token / phoneNumberId 等) */
+    private String configJson;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterChatMsg.java

@@ -0,0 +1,22 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾聊天消息 Domain(对应 chat_msg 表 — 多渠道聚合)
+ */
+@Data
+public class LobsterChatMsg {
+
+    private Long msgId;
+    private Long sessionId;
+    private Long companyId;
+    private String channelType;
+    private String content;
+    private Integer msgType;
+    private Integer sendType;
+    private Integer status;
+    private LocalDateTime createTime;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterChatSession.java

@@ -0,0 +1,28 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾聊天会话 Domain(对应 chat_session 表 — 多渠道聚合)
+ */
+@Data
+public class LobsterChatSession {
+
+    private Long sessionId;
+    private Long companyId;
+    private Long contactId;
+    private String channelType;
+    private String channelSourceId;
+    private String channelSourceType;
+    private String userId;
+    private String externalUserId;
+    private String lastMsg;
+    private LocalDateTime lastMsgTime;
+    private Integer unreadCount;
+    private Integer status;
+    private Long instanceId;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterComplianceAudit.java

@@ -0,0 +1,20 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 合规审计轨迹 Domain(对应 lobster_compliance_audit 表)
+ */
+@Data
+public class LobsterComplianceAudit {
+
+    private Long id;
+    private Long companyId;
+    private String ruleName;
+    private Integer severity;
+    private String matchedKeyword;
+    private String contentSnippet;
+    private LocalDateTime createTime;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterConsumeRecord.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 租户消费记录实体 — 表: tenant_consume_record
+ */
+@Data
+@TableName("tenant_consume_record")
+public class LobsterConsumeRecord {
+
+    @TableId(type = IdType.AUTO)
+    private Long recordId;
+    private Long tenantId;
+    private String tenantName;
+    private Long proxyId;
+    private Integer consumeType;
+    private String consumeTypeName;
+    private BigDecimal amount;
+    private BigDecimal platformCost;
+    private BigDecimal tenantPrice;
+    private BigDecimal proxyRatio;
+    private BigDecimal proxyProfit;
+    private BigDecimal unitPrice;
+    private Integer quantity;
+    private BigDecimal beforeBalance;
+    private BigDecimal afterBalance;
+    private String description;
+    private String month;
+    private String remark;
+    private String orderNo;
+    private Integer status;
+    private String failReason;
+    private LocalDateTime consumeTime;
+    private LocalDateTime createTime;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterDedupConfig.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾消息去重配置
+ */
+@Data
+public class LobsterDedupConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 配置名称 */
+    private String configName;
+    /** 去重模式: exact/semantic/hybrid */
+    private String dedupMode;
+    /** 精确去重窗口大小(消息数) */
+    private Integer exactWindowSize;
+    /** 语义去重相似度阈值(0.0-1.0) */
+    private Double semanticThreshold;
+    /** 去重窗口时长(秒) */
+    private Integer windowDurationSeconds;
+    /** 忽略前缀消息数 */
+    private Integer ignorePrefixCount;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 删除标志 */
+    private Integer delFlag;
+    /** 创建人 */
+    private String createBy;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新人 */
+    private String updateBy;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+    /** 备注 */
+    private String remark;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterEventAudit.java

@@ -0,0 +1,38 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾事件节点审核实体 — 表: lobster_event_node_audit
+ */
+@Data
+@TableName("lobster_event_node_audit")
+public class LobsterEventAudit {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long companyId;
+    private Long instanceId;
+    private String workflowId;
+    private String nodeKey;
+    private String nodeName;
+    private String nodeType;
+    private String nodeJson;
+    private String insertAt;
+    private String insertRefNode;
+    private String decisionEngine;
+    private BigDecimal decisionScore;
+    private String decisionReason;
+    private String status;
+    private String auditBy;
+    private String auditComment;
+    private LocalDateTime auditTime;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterProfileConfig.java

@@ -0,0 +1,40 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾用户画像配置
+ */
+@Data
+public class LobsterProfileConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 配置名称 */
+    private String configName;
+    /** 画像字段映射规则(JSON) */
+    private String fieldMappings;
+    /** 画像来源: crm_order/crm_tag/wechat_profile/custom */
+    private String profileSource;
+    /** 刷新策略: realtime/hourly/daily/manual */
+    private String refreshStrategy;
+    /** 最大画像字段数 */
+    private Integer maxFields;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 删除标志 */
+    private Integer delFlag;
+    /** 创建人 */
+    private String createBy;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新人 */
+    private String updateBy;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+    /** 备注 */
+    private String remark;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterSalesCorpus.java

@@ -0,0 +1,36 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾销冠语料实体 — 表: lobster_sales_corpus
+ */
+@Data
+@TableName("lobster_sales_corpus")
+public class LobsterSalesCorpus {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String dialogText;
+    private String customerQuestion;
+    private String salesAnswer;
+    private String scenario;
+    private BigDecimal score;
+    private String sourceType;
+    private String externalUserId;
+    private Long companyId;
+    private String salespersonName;
+    private String tags;
+    private String status;
+    private Integer enabled;
+    private String createBy;
+    private LocalDateTime createTime;
+    private String updateBy;
+    private LocalDateTime updateTime;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterSummaryConfig.java

@@ -0,0 +1,42 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾摘要生成配置
+ */
+@Data
+public class LobsterSummaryConfig {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 配置名称 */
+    private String configName;
+    /** 触发间隔(消息数) */
+    private Integer triggerInterval;
+    /** 触发策略: message_count/duration/manual */
+    private String triggerStrategy;
+    /** 摘要模型标识符 */
+    private String modelIdentifier;
+    /** 最大上下文消息数 */
+    private Integer maxContextMessages;
+    /** 摘要最大长度(字符) */
+    private Integer maxSummaryLength;
+    /** 是否启用: 0-禁用 1-启用 */
+    private Integer enabled;
+    /** 删除标志 */
+    private Integer delFlag;
+    /** 创建人 */
+    private String createBy;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+    /** 更新人 */
+    private String updateBy;
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+    /** 备注 */
+    private String remark;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterTenantBalance.java

@@ -0,0 +1,40 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 租户余额实体 — 表: tenant_balance
+ */
+@Data
+@TableName("tenant_balance")
+public class LobsterTenantBalance {
+
+    @TableId(type = IdType.AUTO)
+    private Long balanceId;
+    private Long tenantId;
+    private String tenantName;
+    private BigDecimal totalBalance;
+    private BigDecimal aiModelBalance;
+    private BigDecimal courseFlowBalance;
+    private BigDecimal liveFlowBalance;
+    private BigDecimal tokenBalance;
+    private BigDecimal smsBalance;
+    private BigDecimal manualCallBalance;
+    private BigDecimal aiCallBalance;
+    private BigDecimal wechatHelperBalance;
+    private BigDecimal frozenAmount;
+    private BigDecimal totalRecharge;
+    private BigDecimal totalConsume;
+    private LocalDate accountFeeExpireTime;
+    private Integer status;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+    private BigDecimal tokenCoefficient;
+}

+ 33 - 0
fs-service/src/main/java/com/fs/company/domain/LobsterTokenConsumption.java

@@ -0,0 +1,33 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 龙虾Token消耗记录(存于租户DB,管理端跨租户聚合查询)
+ */
+@Data
+public class LobsterTokenConsumption {
+
+    private Long id;
+    /** 租户ID */
+    private Long companyId;
+    /** 工作流实例ID */
+    private Long instanceId;
+    /** 节点编码 */
+    private String nodeCode;
+    /** 模型标识符 */
+    private String modelIdentifier;
+    /** 消耗类型: prompt/completion */
+    private String consumeType;
+    /** Token数量 */
+    private Integer tokenCount;
+    /** 预估费用 */
+    private BigDecimal estimatedCost;
+    /** 请求时间 */
+    private LocalDateTime requestTime;
+    /** 创建时间 */
+    private LocalDateTime createTime;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java

@@ -21,4 +21,18 @@ public interface CompanyWorkflowLobsterNodeMapper extends BaseMapper<CompanyWork
     int checkNodeGreeting(@Param("templateId") Long templateId);
 
     List<String> selectCallWordsByTemplateId(@Param("templateId") Long templateId);
+
+    /** 动态节点生成:取节点当前排序 */
+    Integer selectSortNoByCode(@Param("workflowId") Long workflowId, @Param("nodeCode") String nodeCode);
+
+    /** 动态节点生成:插入新节点(INSERT IGNORE 防止重复) */
+    int insertDynamicNode(@Param("workflowId") Long workflowId,
+                          @Param("nodeCode") String nodeCode,
+                          @Param("nodeName") String nodeName,
+                          @Param("nodeType") Integer nodeType,
+                          @Param("sortNo") Integer sortNo,
+                          @Param("nextNodeCode") String nextNodeCode,
+                          @Param("messageTemplate") String messageTemplate,
+                          @Param("conditionExpr") String conditionExpr,
+                          @Param("nodeConfig") String nodeConfig);
 }

+ 29 - 0
fs-service/src/main/java/com/fs/company/mapper/CustomerFactMapper.java

@@ -0,0 +1,29 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 客户事实记忆 Mapper(customer_fact 表)
+ * 跨实例长期事实存储:目的地、金额、阶段、关键变量等
+ */
+public interface CustomerFactMapper {
+
+    /** 加载该客户最近20条历史事实(跨实例) */
+    List<Map<String, Object>> selectByUser(@Param("companyId") Long companyId,
+                                            @Param("externalUserId") String externalUserId);
+
+    /** 写入或更新单条事实(含 ON DUPLICATE KEY 更新) */
+    int upsert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("instanceId") Long instanceId,
+               @Param("factKey") String factKey,
+               @Param("factValue") String factValue,
+               @Param("factType") String factType);
+
+    /** 遗忘机制:删除过期事实(超过 expireDays 天未更新且 confidence 低于阈值) */
+    int deleteExpiredFacts(@Param("expireBefore") String expireBefore,
+                           @Param("maxConfidence") double maxConfidence);
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/CustomerHabitMapper.java

@@ -0,0 +1,25 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 客户沟通习惯 Mapper(customer_habit 表)
+ * 4维度:回复速度、消息长度偏好、Emoji偏好、对话风格
+ */
+public interface CustomerHabitMapper {
+
+    /** 查询客户全部习惯维度(按置信度过滤) */
+    List<Map<String, Object>> selectByUser(@Param("companyId") Long companyId,
+                                            @Param("externalUserId") String externalUserId);
+
+    /** Upsert 习惯:confidence 采用 EWMA 衰减(旧*0.7 + 新*0.3) */
+    int upsert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("habitKey") String habitKey,
+               @Param("habitValue") String habitValue,
+               @Param("confidence") double confidence,
+               @Param("source") String source);
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterApiRegistryMapper.java

@@ -0,0 +1,36 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾外部 API 注册 Mapper(lobster_api_registry 表)
+ */
+public interface LobsterApiRegistryMapper {
+
+    List<Map<String, Object>> selectEnabled(@Param("category") String category);
+
+    int insert(@Param("apiKey") String apiKey,
+               @Param("apiName") String apiName,
+               @Param("category") String category,
+               @Param("provider") String provider,
+               @Param("baseUrl") String baseUrl,
+               @Param("authType") String authType,
+               @Param("authConfig") String authConfig,
+               @Param("timeout") Integer timeout,
+               @Param("priority") Integer priority,
+               @Param("isBackup") Integer isBackup,
+               @Param("description") String description);
+
+    int updateById(@Param("id") Long id,
+                   @Param("apiKey") String apiKey,
+                   @Param("baseUrl") String baseUrl,
+                   @Param("authConfig") String authConfig,
+                   @Param("enabled") Integer enabled);
+
+    int deleteById(@Param("id") Long id);
+
+    int ensureTable();
+}

+ 203 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java

@@ -0,0 +1,203 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾知识版本 / 事件审计 / E2E / 死信等辅助模块 Mapper
+ */
+public interface LobsterAuxiliaryMapper {
+
+    // === lobster_knowledge_version ===
+    Integer selectMaxVersion(@Param("companyId") Long companyId,
+                             @Param("knowledgeId") Long knowledgeId);
+    Map<String, Object> selectKnowledgeById(@Param("companyId") Long companyId,
+                                             @Param("knowledgeId") Long knowledgeId);
+    int insertVersion(@Param("companyId") Long companyId,
+                      @Param("knowledgeId") Long knowledgeId,
+                      @Param("version") Integer version,
+                      @Param("title") String title,
+                      @Param("content") String content,
+                      @Param("changeLog") String changeLog);
+    List<Map<String, Object>> selectVersions(@Param("companyId") Long companyId,
+                                              @Param("knowledgeId") Long knowledgeId);
+    Map<String, Object> selectVersion(@Param("companyId") Long companyId,
+                                       @Param("knowledgeId") Long knowledgeId,
+                                       @Param("version") Integer version);
+    int rollbackVersion(@Param("companyId") Long companyId,
+                        @Param("knowledgeId") Long knowledgeId,
+                        @Param("toVersion") Integer toVersion);
+    int restoreVersion(@Param("companyId") Long companyId,
+                       @Param("knowledgeId") Long knowledgeId,
+                       @Param("fromVersion") Integer fromVersion);
+    int ensureVersionTable();
+
+    // === lobster_event_node_audit ===
+    List<Map<String, Object>> selectEventStuckNodes(@Param("companyId") Long companyId,
+                                                      @Param("sinceMinutes") int sinceMinutes);
+    List<Map<String, Object>> selectEventNodeStats(@Param("companyId") Long companyId);
+    int insertEventAudit(@Param("companyId") Long companyId,
+                         @Param("nodeCode") String nodeCode,
+                         @Param("stuckCount") int stuckCount,
+                         @Param("avgWaitMs") long avgWaitMs);
+    int ensureEventAuditTable();
+
+    // === lobster_user_events ===
+    int markEventProcessed(@Param("contactId") Long contactId);
+    Long countUserEvents(@Param("companyId") Long companyId);
+
+    // === lobster_dynamic_node_impl / company_dynamic_impl ===
+    List<Map<String, Object>> selectDynamicImpls(@Param("companyId") Long companyId,
+                                                  @Param("nodeType") Integer nodeType);
+    int insertDynamicImpl(@Param("companyId") Long companyId,
+                          @Param("nodeType") Integer nodeType,
+                          @Param("implName") String implName,
+                          @Param("implCode") String implCode,
+                          @Param("scriptContent") String scriptContent,
+                          @Param("status") String status);
+    Long selectLastInsertId();
+    Double selectAvgQualityScore(@Param("nodeType") Integer nodeType,
+                                  @Param("companyId") Long companyId);
+    String selectStatus(@Param("implId") Long implId);
+    int updateStatus(@Param("implId") Long implId, @Param("status") String status);
+    int updateDynamicImpl(@Param("implId") Long implId,
+                          @Param("scriptContent") String scriptContent,
+                          @Param("status") String status);
+    int deleteDynamicImpl(@Param("implId") Long implId);
+    int disableDynamicImpl(@Param("implId") Long implId);
+    List<Map<String, Object>> selectDynamicImplPaged(@Param("companyId") Long companyId,
+                                                      @Param("offset") int offset,
+                                                      @Param("limit") int limit);
+
+    // === lobster_distributed_patterns / practices ===
+    int upsertDistributedPattern(@Param("companyId") Long companyId,
+                                 @Param("industry") String industry,
+                                 @Param("scenario") String scenario,
+                                 @Param("patternType") String patternType,
+                                 @Param("content") String content,
+                                 @Param("score") double score);
+    List<Map<String, Object>> selectDistributedPatterns(@Param("companyId") Long companyId,
+                                                          @Param("industry") String industry,
+                                                          @Param("scenario") String scenario);
+    Map<String, Object> selectDistributedPractice(@Param("practiceId") Long practiceId);
+    int incrementPracticeUsage(@Param("practiceId") Long practiceId);
+    Integer countDistributedPatterns(@Param("companyId") Long companyId);
+    Integer countDistributedPractices(@Param("companyId") Long companyId);
+    Double avgDistributedScore(@Param("companyId") Long companyId);
+    Integer countDistributedContributors(@Param("companyId") Long companyId);
+    int ensureDistPatternTable();
+    int ensureDistPracticeTable();
+
+    // === lobster_heartbeat / scheduler ===
+    int insertHeartbeat(@Param("companyId") Long companyId,
+                        @Param("taskKey") String taskKey,
+                        @Param("status") String status);
+    List<Map<String, Object>> selectHeartbeats(@Param("companyId") Long companyId,
+                                                @Param("taskKey") String taskKey,
+                                                @Param("limit") int limit);
+    int updateHeartbeat(@Param("companyId") Long companyId,
+                        @Param("taskKey") String taskKey,
+                        @Param("status") String status);
+    int ensureHeartbeatTable();
+
+    // === lobster_dead_letter_queue ===
+    int insertDeadLetter(@Param("companyId") Long companyId,
+                         @Param("queueName") String queueName,
+                         @Param("payload") String payload,
+                         @Param("error") String error);
+    List<Map<String, Object>> selectDeadLetters(@Param("queueName") String queueName,
+                                                 @Param("limit") int limit);
+    int retryDeadLetter(@Param("id") Long id);
+    int deleteDeadLetter(@Param("id") Long id);
+    int ensureDeadLetterTable();
+
+    // === lobster_e2e_test ===
+    int insertE2eTest(@Param("companyId") Long companyId,
+                      @Param("testName") String testName,
+                      @Param("workflowId") Long workflowId,
+                      @Param("testData") String testData);
+    int insertE2eResult(@Param("testId") Long testId,
+                        @Param("companyId") Long companyId,
+                        @Param("passed") boolean passed,
+                        @Param("detail") String detail);
+    List<Map<String, Object>> selectE2eTests(@Param("companyId") Long companyId,
+                                              @Param("offset") int offset,
+                                              @Param("limit") int limit);
+    Map<String, Object> selectE2eResult(@Param("runId") String runId);
+    List<Map<String, Object>> selectE2eList(@Param("testId") Long testId);
+    int ensureE2eTestTable();
+    int ensureE2eResultTable();
+
+    // === lobster_test_scenario ===
+    List<Map<String, Object>> selectTestScenarios(@Param("companyId") Long companyId);
+    Map<String, Object> selectTestScenarioById(@Param("id") Long id,
+                                                @Param("companyId") Long companyId);
+    int insertTestScenario(@Param("companyId") Long companyId,
+                           @Param("scenarioName") String scenarioName,
+                           @Param("workflowId") Long workflowId,
+                           @Param("testData") String testData);
+    int updateTestScenario(@Param("id") Long id,
+                           @Param("scenarioName") String scenarioName,
+                           @Param("testData") String testData);
+    int deleteTestScenario(@Param("id") Long id, @Param("companyId") Long companyId);
+    int insertTestScenarioResult(@Param("companyId") Long companyId,
+                                  @Param("scenarioId") Long scenarioId,
+                                  @Param("passed") boolean passed,
+                                  @Param("detail") String detail);
+    int ensureScenarioTable();
+
+    // === lobster_workflow_patch ===
+    int insertPatch(@Param("companyId") Long companyId,
+                    @Param("targetTable") String targetTable,
+                    @Param("targetId") Long targetId,
+                    @Param("fieldName") String fieldName,
+                    @Param("oldValue") String oldValue,
+                    @Param("newValue") String newValue,
+                    @Param("reason") String reason);
+    List<Map<String, Object>> selectPatches(@Param("companyId") Long companyId);
+    int applyPatch(@Param("id") Long id);
+    int rejectPatch(@Param("id") Long id);
+    int ensurePatchTable();
+
+    // === vector tables ===
+    int insertVectorEmbedding(@Param("companyId") Long companyId,
+                              @Param("docId") String docId,
+                              @Param("docType") String docType,
+                              @Param("content") String content,
+                              @Param("embedding") String embedding);
+    List<Map<String, Object>> selectVectorEmbeddings(@Param("companyId") Long companyId,
+                                                      @Param("docType") String docType);
+    int ensureVectorTable();
+
+    // === 通用 ===
+    List<Map<String, Object>> selectPaged(@Param("table") String table,
+                                           @Param("whereClause") String whereClause,
+                                           @Param("offset") int offset,
+                                           @Param("limit") int limit);
+    int update(@Param("sql") String sql);
+
+    /** 带参数的 update/insert(占位符 ? 由 args 填充) */
+    int updateWithArgs(@Param("sql") String sql, @Param("args") List<Object> args);
+
+    /** ContactAdapter 使用:从平台用户表查原始ID */
+    Map<String, Object> selectSourceUserId(@Param("table") String sourceTable,
+                                            @Param("column") String userIdColumn,
+                                            @Param("companyId") Long companyId);
+
+    /** 配置读取(通用) */
+    String selectConfig(@Param("companyId") Long companyId,
+                        @Param("configKey") String configKey);
+
+    /** 兼容旧 jdbcTemplate.queryForList (单参数 companyId) */
+    List<Map<String, Object>> queryForList(@Param("sql") String sql,
+                                            @Param("companyId") Long companyId);
+
+    /** 兼容旧 jdbcTemplate.queryForList (单参数 + 返回类型) */
+    List<String> queryForStringList(@Param("sql") String sql,
+                                     @Param("companyId") Long companyId);
+
+    /** 兼容旧 jdbcTemplate.queryForObject → 表存在检查 */
+    int executeStatement(@Param("sql") String sql);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterBillingMapper.java

@@ -0,0 +1,27 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterConsumeRecord;
+import com.fs.company.domain.LobsterTenantBalance;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 龙虾计费 Mapper — 表: tenant_balance / tenant_consume_record
+ */
+public interface LobsterBillingMapper {
+
+    // ======== tenant_balance ========
+    LobsterTenantBalance selectBalanceByTenantId(@Param("tenantId") Long tenantId);
+
+    int updateTokenCoefficient(@Param("tenantId") Long tenantId,
+                               @Param("coefficient") BigDecimal coefficient);
+
+    // ======== tenant_consume_record ========
+    List<LobsterConsumeRecord> selectConsumeRecords(@Param("tenantId") Long tenantId,
+                                                     @Param("offset") int offset,
+                                                     @Param("limit") int limit);
+
+    long countConsumeRecords(@Param("tenantId") Long tenantId);
+}

+ 44 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChannelPluginConfigMapper.java

@@ -0,0 +1,44 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterChannelPluginConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾渠道插件配置 Mapper
+ * <p>
+ * 表: lobster_channel_plugin_config
+ */
+public interface LobsterChannelPluginConfigMapper {
+
+    /**
+     * 查询租户指定渠道的配置
+     */
+    LobsterChannelPluginConfig selectByCompanyAndChannel(@Param("companyId") Long companyId,
+                                                          @Param("channelType") String channelType);
+
+    /**
+     * 查询租户所有已配置的渠道
+     */
+    List<LobsterChannelPluginConfig> selectByCompanyId(@Param("companyId") Long companyId);
+
+    /**
+     * 插入或更新(ON DUPLICATE KEY UPDATE)
+     */
+    int upsert(LobsterChannelPluginConfig config);
+
+    /**
+     * 更新启用状态
+     */
+    int updateEnabled(@Param("companyId") Long companyId,
+                      @Param("channelType") String channelType,
+                      @Param("enabled") Integer enabled);
+
+    /**
+     * 更新配置 JSON
+     */
+    int updateConfig(@Param("companyId") Long companyId,
+                     @Param("channelType") String channelType,
+                     @Param("configJson") String configJson);
+}

+ 35 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChannelRegistryMapper.java

@@ -0,0 +1,35 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 渠道注册 + 统一触点 Mapper
+ * 表: lobster_channel_type_registry / lobster_unified_contact
+ */
+public interface LobsterChannelRegistryMapper {
+
+    /** 从 unified_contact 查渠道用户ID */
+    String selectChannelUserId(@Param("companyId") Long companyId,
+                                @Param("contactId") Long contactId,
+                                @Param("channelType") String channelType);
+
+    /** 从平台用户表查原始ID */
+    Map<String, Object> selectSourceUserId(@Param("table") String sourceTable,
+                                            @Param("column") String userIdColumn,
+                                            @Param("companyId") Long companyId);
+
+    /** 绑定触点 */
+    int upsertUnifiedContact(@Param("companyId") Long companyId,
+                             @Param("contactId") Long contactId,
+                             @Param("channelType") String channelType,
+                             @Param("channelUserId") String channelUserId);
+
+    /** 加载DB渠道注册 */
+    List<Map<String, Object>> selectEnabledChannels();
+
+    /** 建表探针 */
+    int ensureTable();
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChatMsgMapper.java

@@ -0,0 +1,18 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterChatMsg;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾聊天消息 Mapper(chat_msg 表 — 多渠道聚合)
+ */
+public interface LobsterChatMsgMapper {
+
+    /** 插入消息 */
+    int insert(LobsterChatMsg msg);
+
+    /** 按 sessionId 查询消息列表 */
+    List<LobsterChatMsg> selectBySessionId(@Param("sessionId") Long sessionId);
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChatRecordMapper.java

@@ -0,0 +1,28 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾聊天记录 Mapper(lobster_chat_record 表)
+ */
+public interface LobsterChatRecordMapper {
+
+    /** 统计最近N分钟新增的消息数 */
+    Integer countNewMessages(@Param("externalUserId") String externalUserId,
+                              @Param("instanceId") Long instanceId,
+                              @Param("minutes") int minutes);
+
+    /** 获取上次摘要后的新对话 */
+    List<Map<String, Object>> selectNewChatsSince(@Param("externalUserId") String externalUserId,
+                                                    @Param("instanceId") Long instanceId,
+                                                    @Param("lastSummaryTime") String lastSummaryTime);
+
+    /** ContextAssembler 使用:加载最近 N 条对话(按 message_time 倒序) */
+    List<Map<String, Object>> selectRecentChats(@Param("externalUserId") String externalUserId,
+                                                 @Param("instanceId") Long instanceId,
+                                                 @Param("companyId") Long companyId,
+                                                 @Param("limit") int limit);
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterChatSessionMapper.java

@@ -0,0 +1,36 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterChatSession;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾聊天会话 Mapper(chat_session 表 — 多渠道聚合)
+ */
+public interface LobsterChatSessionMapper {
+
+    /** 按 company_id + channel_source_id + channel_type 查询 */
+    LobsterChatSession selectBySourceAndChannel(@Param("companyId") Long companyId,
+                                                 @Param("channelSourceId") String channelSourceId,
+                                                 @Param("channelType") String channelType);
+
+    /** 按 company_id + user_id 查询(降级兼容) */
+    LobsterChatSession selectByCompanyAndUser(@Param("companyId") Long companyId,
+                                              @Param("userId") String userId);
+
+    /** 插入新会话,返回 sessionId */
+    int insert(LobsterChatSession session);
+
+    /** 更新最后消息 */
+    int updateLastMsg(@Param("sessionId") Long sessionId,
+                      @Param("lastMsg") String lastMsg);
+
+    /** 渠道聚合列表查询(带筛选) */
+    List<LobsterChatSession> selectForAggregate(@Param("channelType") String channelType,
+                                                 @Param("keyword") String keyword);
+
+    /** 按 sessionId 查询单个会话 */
+    LobsterChatSession selectBySessionId(@Param("sessionId") Long sessionId);
+}

+ 14 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceAuditMapper.java

@@ -0,0 +1,14 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterComplianceAudit;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 合规审计 Mapper(lobster_compliance_audit 表)
+ */
+public interface LobsterComplianceAuditMapper {
+
+    int insert(LobsterComplianceAudit audit);
+
+    int ensureTable();
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceRuleMapper.java

@@ -18,4 +18,6 @@ public interface LobsterComplianceRuleMapper {
     int updateById(LobsterComplianceRule rule);
 
     int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    int ensureTable();
 }

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterConversationSummaryMapper.java

@@ -14,4 +14,29 @@ public interface LobsterConversationSummaryMapper {
     int insert(LobsterConversationSummary summary);
 
     int updateById(LobsterConversationSummary summary);
+
+    /** 全局摘要:按 external_user_id 获取最近10条会话摘要 */
+    List<LobsterConversationSummary> selectByExternalUserId(@Param("companyId") Long companyId,
+                                                             @Param("externalUserId") String externalUserId);
+
+    /** 仅取最近一条 summary_text(按 user+instance) */
+    String selectLatestSummaryText(@Param("externalUserId") String externalUserId,
+                                    @Param("instanceId") Long instanceId);
+
+    /** 自动摘要写入(含 stage / nextActionHint / keyIntents / keyVariables 等扩展列) */
+    int insertAutoSummary(@Param("companyId") Long companyId,
+                          @Param("externalUserId") String externalUserId,
+                          @Param("instanceId") Long instanceId,
+                          @Param("summaryText") String summaryText,
+                          @Param("chatMsgCount") int chatMsgCount,
+                          @Param("keyIntents") String keyIntents,
+                          @Param("keyVariables") String keyVariables,
+                          @Param("stage") String stage,
+                          @Param("nextActionHint") String nextActionHint);
+
+    /** 摘要写入后回写用户画像状态 */
+    int updateUserProfile(@Param("companyId") Long companyId,
+                          @Param("externalUserId") String externalUserId,
+                          @Param("currentState") String currentState,
+                          @Param("variableSnapshot") String variableSnapshot);
 }

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterDedupConfigMapper.java

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterDedupConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 消息去重配置 Mapper
+ */
+public interface LobsterDedupConfigMapper {
+
+    LobsterDedupConfig selectByCompanyId(@Param("companyId") Long companyId);
+
+    List<LobsterDedupConfig> selectAll(@Param("companyId") Long companyId);
+
+    int insert(LobsterDedupConfig config);
+
+    int updateById(LobsterDedupConfig config);
+
+    int deleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+}

+ 24 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterDialogueStateMapper.java

@@ -0,0 +1,24 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * 龙虾对话状态 Mapper(lobster_dialogue_state 表 — 断点续聊)
+ */
+public interface LobsterDialogueStateMapper {
+
+    /** 读最新状态 */
+    Map<String, Object> selectLatest(@Param("companyId") Long companyId,
+                                      @Param("externalUserId") String externalUserId);
+
+    /** UPSERT 写入对话状态 */
+    int upsert(@Param("instanceId") Long instanceId,
+               @Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("nodeCode") String nodeCode,
+               @Param("intent") String intent,
+               @Param("sentiment") String sentiment,
+               @Param("lastReply") String lastReply);
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterEventAuditMapper.java

@@ -0,0 +1,28 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterEventAudit;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 龙虾事件节点审核 Mapper — 表: lobster_event_node_audit
+ */
+public interface LobsterEventAuditMapper {
+
+    List<LobsterEventAudit> selectList(@Param("companyId") Long companyId,
+                                        @Param("status") String status,
+                                        @Param("offset") int offset,
+                                        @Param("limit") int limit);
+
+    long countByStatus(@Param("companyId") Long companyId, @Param("status") String status);
+
+    long countTotalByStatus(@Param("companyId") Long companyId, @Param("status") String status);
+
+    LobsterEventAudit selectById(@Param("id") Long id);
+
+    int updateStatus(@Param("id") Long id,
+                     @Param("status") String status,
+                     @Param("auditBy") String auditBy,
+                     @Param("auditComment") String auditComment);
+}

+ 92 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java

@@ -0,0 +1,92 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾进化配置 Mapper
+ * 表: lobster_node_optimization / lobster_optimization_config / lobster_node_interaction
+ */
+public interface LobsterEvolutionConfigMapper {
+
+    List<Map<String, Object>> selectOptimizationStats(@Param("companyId") Long companyId);
+    List<Map<String, Object>> selectOptimizationTypes(@Param("companyId") Long companyId);
+    List<Map<String, Object>> selectOptimizations(@Param("companyId") Long companyId,
+                                                   @Param("workflowId") Long workflowId,
+                                                   @Param("offset") int offset,
+                                                   @Param("limit") int limit);
+    List<Map<String, Object>> selectOptimizationByStatus(@Param("companyId") Long companyId,
+                                                          @Param("status") String status,
+                                                          @Param("offset") int offset,
+                                                          @Param("limit") int limit);
+    Map<String, Object> selectOptimizationById(@Param("id") Long id,
+                                                @Param("companyId") Long companyId);
+    int approveOptimization(@Param("id") Long id, @Param("auditorId") Long auditorId);
+    int rejectOptimization(@Param("id") Long id, @Param("auditorId") Long auditorId);
+    int ensureOptimizationTable();
+
+    Map<String, Object> selectConfig(@Param("companyId") Long companyId,
+                                      @Param("workflowId") Long workflowId,
+                                      @Param("nodeCode") String nodeCode);
+    Boolean selectConfigEnabled(@Param("companyId") Long companyId,
+                                 @Param("workflowId") Long workflowId,
+                                 @Param("nodeCode") String nodeCode);
+    int upsertConfig(@Param("companyId") Long companyId,
+                     @Param("workflowId") Long workflowId,
+                     @Param("nodeCode") String nodeCode,
+                     @Param("enabled") Boolean enabled,
+                     @Param("autoApply") String autoApply,
+                     @Param("configJson") String configJson);
+    int updateConfigStatus(@Param("companyId") Long companyId,
+                           @Param("workflowId") Long workflowId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("enabled") Boolean enabled);
+    int ensureConfigTable();
+
+    int insertInteraction(@Param("companyId") Long companyId,
+                          @Param("instanceId") Long instanceId,
+                          @Param("nodeCode") String nodeCode,
+                          @Param("externalUserId") String externalUserId,
+                          @Param("interactionType") String interactionType,
+                          @Param("content") String content);
+    List<String> selectInteractionTrace(@Param("externalUserId") String externalUserId,
+                                         @Param("nodeCode") String nodeCode,
+                                         @Param("companyId") Long companyId);
+    int ensureInteractionTable();
+
+    Double selectAverageQualityScore(@Param("companyId") Long companyId,
+                                      @Param("nodeCode") String nodeCode);
+
+    // === lobster_evolution_log (EvolutionEngineImpl) ===
+    int insertEvolutionLog(@Param("companyId") Long companyId,
+                           @Param("workflowId") Long workflowId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("action") String action,
+                           @Param("detail") String detail);
+    List<Map<String, Object>> selectEvolutionLogs(@Param("companyId") Long companyId,
+                                                   @Param("workflowId") Long workflowId);
+    int ensureEvolutionLogTable();
+
+    // === lobster_evolution_suggestion (EvolutionEngineImpl) ===
+    Map<String, Object> selectSuggestionById(@Param("id") Long id,
+                                              @Param("companyId") Long companyId);
+    int insertSuggestion(@Param("companyId") Long companyId,
+                         @Param("workflowId") Long workflowId,
+                         @Param("nodeCode") String nodeCode,
+                         @Param("suggestionType") String suggestionType,
+                         @Param("currentContent") String currentContent,
+                         @Param("suggestedContent") String suggestedContent,
+                         @Param("reason") String reason,
+                         @Param("confidence") Double confidence);
+    int updateNodeMessage(@Param("suggestedContent") String suggestedContent,
+                          @Param("workflowId") Long workflowId,
+                          @Param("nodeCode") String nodeCode);
+    int acceptSuggestion(@Param("id") Long id);
+    Integer countTotal(@Param("companyId") Long companyId);
+    Integer countReplied(@Param("companyId") Long companyId);
+    Integer countPendingSuggestion(@Param("companyId") Long companyId);
+    Integer countApplied(@Param("companyId") Long companyId);
+    int ensureSuggestionTable();
+}

+ 57 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterFeedbackMapper.java

@@ -0,0 +1,57 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾反馈驱动进化 Mapper
+ * 表: lobster_feedback_records / lobster_message_variants / lobster_ab_tests
+ */
+public interface LobsterFeedbackMapper {
+
+    // ===== feedback_records =====
+    int insertFeedback(@Param("companyId") Long companyId,
+                       @Param("instanceId") Long instanceId,
+                       @Param("nodeCode") String nodeCode,
+                       @Param("feedbackType") String feedbackType,
+                       @Param("comment") String comment);
+
+    List<Map<String, Object>> selectFeedbackStats(@Param("companyId") Long companyId);
+
+    String selectCurrentMessage(@Param("nodeCode") String nodeCode,
+                                @Param("companyId") Long companyId);
+
+    List<String> selectNegativeComments(@Param("companyId") Long companyId,
+                                         @Param("nodeCode") String nodeCode);
+
+    int ensureFeedbackTable();
+
+    // ===== message_variants =====
+    int insertVariant(@Param("companyId") Long companyId,
+                      @Param("nodeCode") String nodeCode,
+                      @Param("content") String content,
+                      @Param("generationReason") String generationReason);
+
+    int ensureVariantTable();
+
+    // ===== ab_tests =====
+    List<Map<String, Object>> selectActiveAbTests(@Param("companyId") Long companyId);
+
+    Map<String, Object> selectAbTest(@Param("testId") Long testId,
+                                      @Param("companyId") Long companyId);
+
+    int applyVariantToNode(@Param("variantMessage") String variantMessage,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("companyId") Long companyId);
+
+    int markAbTestApplied(@Param("testId") Long testId);
+
+    int markAbTestCompleted(@Param("testId") Long testId);
+
+    List<Map<String, Object>> selectWinAbTests(@Param("companyId") Long companyId,
+                                                @Param("winThreshold") double winThreshold);
+
+    int ensureAbTestTable();
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterHandoffEventMapper.java

@@ -0,0 +1,15 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 龙虾转人工事件 Mapper(lobster_handoff_events 表)
+ */
+public interface LobsterHandoffEventMapper {
+
+    int insertEvent(@Param("companyId") Long companyId,
+                    @Param("instanceId") Long instanceId,
+                    @Param("externalUserId") String externalUserId,
+                    @Param("triggerType") String triggerType,
+                    @Param("triggerDetail") String triggerDetail);
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterKnowledgeUsageLogMapper.java

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * 知识使用反馈 Mapper(lobster_knowledge_usage_log 表)
+ * 追踪知识被检索后在 AI 回复中的实际使用情况
+ */
+public interface LobsterKnowledgeUsageLogMapper {
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("instanceId") Long instanceId,
+               @Param("knowledgeTitle") String knowledgeTitle,
+               @Param("knowledgeText") String knowledgeText,
+               @Param("retrievalMethod") String retrievalMethod,
+               @Param("source") String source);
+
+    Map<String, Object> countUsage(@Param("companyId") Long companyId,
+                                    @Param("knowledgeTitle") String knowledgeTitle);
+}

+ 35 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterLearningCorpusMapper.java

@@ -0,0 +1,35 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 销冠语料库 Mapper(lobster_learning_corpus 表)
+ */
+public interface LobsterLearningCorpusMapper {
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("salespersonName") String salespersonName,
+               @Param("customerQuestion") String customerQuestion,
+               @Param("salesAnswer") String salesAnswer,
+               @Param("scenario") String scenario,
+               @Param("industryType") String industryType,
+               @Param("tags") String tags,
+               @Param("status") String status);
+
+    List<Map<String, Object>> selectByScenario(@Param("companyId") Long companyId,
+                                                @Param("scenario") String scenario,
+                                                @Param("limit") int limit);
+
+    int incrementUsageCount(@Param("entryId") Long entryId);
+
+    List<Map<String, Object>> selectRawCorpus(@Param("companyId") Long companyId,
+                                               @Param("limit") int limit);
+
+    int markAnalyzed(@Param("companyId") Long companyId,
+                     @Param("count") int count);
+
+    int ensureTable();
+}

+ 65 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterMultiTurnDialogueMapper.java

@@ -0,0 +1,65 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾多轮对话管理 Mapper(lobster_multi_turn_dialogue 表)
+ */
+public interface LobsterMultiTurnDialogueMapper {
+
+    int deleteState(@Param("instanceId") Long instanceId,
+                    @Param("nodeCode") String nodeCode,
+                    @Param("companyId") Long companyId);
+
+    int deleteStateNoCompany(@Param("instanceId") Long instanceId,
+                             @Param("nodeCode") String nodeCode);
+
+    List<Map<String, Object>> selectDialogue(@Param("instanceId") Long instanceId,
+                                              @Param("nodeCode") String nodeCode,
+                                              @Param("companyId") Long companyId);
+
+    List<Map<String, Object>> selectDialogueNoCompany(@Param("instanceId") Long instanceId,
+                                                       @Param("nodeCode") String nodeCode);
+
+    int appendTurn(@Param("companyId") Long companyId,
+                   @Param("instanceId") Long instanceId,
+                   @Param("nodeCode") String nodeCode,
+                   @Param("turnIndex") int turnIndex,
+                   @Param("direction") int direction,
+                   @Param("content") String content);
+
+    int appendTurnNoCompany(@Param("instanceId") Long instanceId,
+                            @Param("nodeCode") String nodeCode,
+                            @Param("turnIndex") int turnIndex,
+                            @Param("direction") int direction,
+                            @Param("content") String content);
+
+    int getMaxTurnIndex(@Param("instanceId") Long instanceId,
+                        @Param("nodeCode") String nodeCode,
+                        @Param("companyId") Long companyId);
+
+    int ensureTable();
+
+    /** 按 state_json 方式存取 */
+    String selectStateJson(@Param("instanceId") Long instanceId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("companyId") Long companyId);
+
+    String selectStateJsonNoCompany(@Param("instanceId") Long instanceId,
+                                    @Param("nodeCode") String nodeCode);
+
+    int upsertState(@Param("companyId") Long companyId,
+                    @Param("instanceId") Long instanceId,
+                    @Param("nodeCode") String nodeCode,
+                    @Param("stateJson") String stateJson);
+
+    int upsertStateNoCompany(@Param("instanceId") Long instanceId,
+                             @Param("nodeCode") String nodeCode,
+                             @Param("stateJson") String stateJson);
+
+    List<Map<String, Object>> selectNodeConfig(@Param("instanceId") Long instanceId,
+                                                @Param("nodeCode") String nodeCode);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterNodeExecutionLogMapper.java

@@ -14,4 +14,7 @@ public interface LobsterNodeExecutionLogMapper {
     int insert(LobsterNodeExecutionLog log);
 
     int updateById(LobsterNodeExecutionLog log);
+
+    /** 查询节点历史平均质量评分 */
+    Integer selectAverageQualityScore(@Param("companyId") Long companyId, @Param("nodeCode") String nodeCode);
 }

+ 54 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterPendingKnowledgeMapper.java

@@ -0,0 +1,54 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾待审知识库 Mapper(lobster_pending_knowledge 表)
+ */
+public interface LobsterPendingKnowledgeMapper {
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("knowledgeType") String knowledgeType,
+               @Param("content") String content,
+               @Param("contextSnapshot") String contextSnapshot,
+               @Param("sourceNodeCode") String sourceNodeCode,
+               @Param("status") String status);
+
+    List<Map<String, Object>> selectByCompany(@Param("companyId") Long companyId,
+                                               @Param("status") String status,
+                                               @Param("offset") int offset,
+                                               @Param("limit") int limit);
+
+    int countByCompany(@Param("companyId") Long companyId,
+                       @Param("status") String status);
+
+    List<Map<String, Object>> selectById(@Param("id") Long id,
+                                          @Param("companyId") Long companyId);
+
+    int auditApprove(@Param("id") Long id,
+                     @Param("companyId") Long companyId,
+                     @Param("auditorId") Long auditorId,
+                     @Param("auditComment") String auditComment);
+
+    int auditReject(@Param("id") Long id,
+                    @Param("companyId") Long companyId,
+                    @Param("auditorId") Long auditorId,
+                    @Param("auditComment") String auditComment);
+
+    int batchApprove(@Param("ids") List<Long> ids,
+                     @Param("companyId") Long companyId,
+                     @Param("auditorId") Long auditorId);
+
+    int batchReject(@Param("ids") List<Long> ids,
+                    @Param("companyId") Long companyId,
+                    @Param("auditorId") Long auditorId);
+
+    int delete(@Param("id") Long id,
+               @Param("companyId") Long companyId);
+
+    int ensureTable();
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterProfileConfigMapper.java

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterProfileConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 用户画像配置 Mapper
+ */
+public interface LobsterProfileConfigMapper {
+
+    LobsterProfileConfig selectByCompanyId(@Param("companyId") Long companyId);
+
+    List<LobsterProfileConfig> selectAll(@Param("companyId") Long companyId);
+
+    int insert(LobsterProfileConfig config);
+
+    int updateById(LobsterProfileConfig config);
+
+    int deleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+}

+ 31 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSalesCorpusMapper.java

@@ -0,0 +1,31 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾销售语料分析 Mapper(lobster_sales_corpus 表)
+ */
+public interface LobsterSalesCorpusMapper {
+
+    List<Map<String, Object>> selectByScenario(@Param("companyId") Long companyId,
+                                                @Param("scenario") String scenario,
+                                                @Param("limit") int limit);
+
+    int incrementUsageCount(@Param("entryId") Long entryId);
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("scenario") String scenario,
+               @Param("content") String content,
+               @Param("source") String source,
+               @Param("score") double score);
+
+    List<Map<String, Object>> selectAll(@Param("companyId") Long companyId);
+
+    int updateScore(@Param("entryId") Long entryId,
+                    @Param("score") double score);
+
+    int ensureTable();
+}

+ 31 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSegmentMapper.java

@@ -0,0 +1,31 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 用户分群 / 个性化配置 Mapper
+ * 表: lobster_user_segment / lobster_segment_config / lobster_segment_message_override
+ */
+public interface LobsterSegmentMapper {
+
+    List<Map<String, Object>> selectUserSegments(@Param("companyId") Long companyId,
+                                                  @Param("externalUserId") String externalUserId);
+
+    int saveUserSegment(@Param("companyId") Long companyId,
+                        @Param("externalUserId") String externalUserId,
+                        @Param("segmentCode") String segmentCode);
+
+    Map<String, Object> selectSegmentConfig(@Param("companyId") Long companyId,
+                                             @Param("segmentCode") String segmentCode);
+
+    /** 分群话术覆盖 */
+    String selectMessageOverride(@Param("companyId") Long companyId,
+                                 @Param("nodeCode") String nodeCode,
+                                 @Param("segmentCode") String segmentCode);
+
+    int ensureSegmentTable();
+    int ensureUserSegmentTable();
+}

+ 26 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSensitiveWordMapper.java

@@ -0,0 +1,26 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾敏感词 Mapper(lobster_sensitive_word 表)
+ */
+public interface LobsterSensitiveWordMapper {
+
+    int insert(@Param("companyId") Long companyId,
+               @Param("word") String word,
+               @Param("level") String level,
+               @Param("replacement") String replacement,
+               @Param("enabled") Integer enabled);
+
+    int disable(@Param("id") Long id);
+
+    List<Map<String, Object>> selectAll(@Param("companyId") Long companyId);
+
+    List<Map<String, Object>> selectEnabled(@Param("companyId") Long companyId);
+
+    int ensureTable();
+}

+ 13 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSmartApiMapper.java

@@ -0,0 +1,13 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * 龙虾智能 API 配置 Mapper(lobster_smart_api 表)
+ */
+public interface LobsterSmartApiMapper {
+
+    Map<String, Object> selectByCode(@Param("apiCode") String apiCode);
+}

+ 22 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSummaryConfigMapper.java

@@ -0,0 +1,22 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterSummaryConfig;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 摘要生成配置 Mapper
+ */
+public interface LobsterSummaryConfigMapper {
+
+    LobsterSummaryConfig selectByCompanyId(@Param("companyId") Long companyId);
+
+    List<LobsterSummaryConfig> selectAll(@Param("companyId") Long companyId);
+
+    int insert(LobsterSummaryConfig config);
+
+    int updateById(LobsterSummaryConfig config);
+
+    int deleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+}

+ 21 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterSystemPromptMapper.java

@@ -26,4 +26,25 @@ public interface LobsterSystemPromptMapper {
     int softDeleteById(@Param("id") Long id);
 
     List<String> selectCategories();
+
+    /** PromptManagerImpl 使用:三级优先级查询 */
+    String selectByPriority(@Param("companyId") Long companyId,
+                            @Param("workflowCode") String workflowCode,
+                            @Param("nodeCode") String nodeCode,
+                            @Param("promptType") String promptType);
+
+    int upsertNodePrompt(@Param("companyId") Long companyId,
+                         @Param("workflowCode") String workflowCode,
+                         @Param("nodeCode") String nodeCode,
+                         @Param("promptType") String promptType,
+                         @Param("content") String content);
+
+    int upsertGlobalPrompt(@Param("companyId") Long companyId,
+                           @Param("promptType") String promptType,
+                           @Param("content") String content);
+
+    List<LobsterSystemPrompt> selectByScope(@Param("scope") String scope,
+                                            @Param("companyId") Long companyId);
+
+    int ensureTable();
 }

+ 93 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterTenantLearningMapper.java

@@ -0,0 +1,93 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾租户学习 / 公司配置 Mapper
+ * 表: lobster_learning_event_log / lobster_learned_pattern / lobster_learning_replay_buffer / company_config
+ */
+public interface LobsterTenantLearningMapper {
+
+    int insertEventLog(@Param("companyId") Long companyId,
+                       @Param("instanceId") Long instanceId,
+                       @Param("nodeCode") String nodeCode,
+                       @Param("eventType") String eventType,
+                       @Param("qualityScore") Integer qualityScore,
+                       @Param("contextSnapshot") String contextSnapshot);
+    int countQualityEvents(@Param("companyId") Long companyId,
+                           @Param("eventType") String eventType);
+
+    int upsertPattern(@Param("companyId") Long companyId,
+                      @Param("patternType") String patternType,
+                      @Param("patternKey") String patternKey,
+                      @Param("patternValue") String patternValue,
+                      @Param("confidence") double confidence,
+                      @Param("source") String source);
+    List<Map<String, Object>> selectPatterns(@Param("companyId") Long companyId);
+    List<Map<String, Object>> selectPatternsByScenario(@Param("companyId") Long companyId,
+                                                        @Param("scenario") String scenario);
+    int ensurePatternTable();
+
+    int insertReplayBuffer(@Param("companyId") Long companyId,
+                           @Param("instanceId") Long instanceId,
+                           @Param("nodeCode") String nodeCode,
+                           @Param("customerMessage") String customerMessage,
+                           @Param("aiReply") String aiReply,
+                           @Param("qualityScore") Integer qualityScore);
+    List<Map<String, Object>> selectReplayBuffer(@Param("companyId") Long companyId);
+    int cleanupReplayBuffer(@Param("companyId") Long companyId,
+                            @Param("before") String before);
+    int ensureReplayTable();
+
+    String selectConfig(@Param("companyId") Long companyId,
+                        @Param("configKey") String configKey);
+    int upsertConfig(@Param("companyId") Long companyId,
+                     @Param("configKey") String configKey,
+                     @Param("configValue") String configValue);
+    List<Map<String, Object>> selectAllConfigs(@Param("companyId") Long companyId);
+
+    /** 画像配置 CRUD */
+    List<Map<String, Object>> selectProfileConfigs(@Param("companyId") Long companyId);
+    int insertProfileConfig(@Param("companyId") Long companyId,
+                            @Param("fieldKey") String fieldKey,
+                            @Param("fieldLabel") String fieldLabel,
+                            @Param("fieldType") String fieldType);
+    int updateProfileConfig(@Param("id") Long id,
+                            @Param("fieldKey") String fieldKey,
+                            @Param("fieldLabel") String fieldLabel,
+                            @Param("fieldType") String fieldType);
+    int deleteProfileConfig(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    /** 摘要配置 CRUD */
+    List<Map<String, Object>> selectSummaryConfigs(@Param("companyId") Long companyId);
+    int insertSummaryConfig(@Param("companyId") Long companyId,
+                            @Param("scenario") String scenario,
+                            @Param("summaryTemplate") String summaryTemplate);
+    int updateSummaryConfig(@Param("id") Long id,
+                            @Param("scenario") String scenario,
+                            @Param("summaryTemplate") String summaryTemplate);
+    int deleteSummaryConfig(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    /** 敏感词配置 CRUD */
+    List<Map<String, Object>> selectSensitiveWords(@Param("companyId") Long companyId);
+    int insertSensitiveWord(@Param("companyId") Long companyId,
+                            @Param("word") String word,
+                            @Param("category") String category,
+                            @Param("action") String action,
+                            @Param("enabled") Integer enabled);
+    int updateSensitiveWord(@Param("id") Long id,
+                            @Param("word") String word,
+                            @Param("category") String category,
+                            @Param("action") String action,
+                            @Param("enabled") Integer enabled);
+    int deleteSensitiveWord(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    /** 分页查询 */
+    List<Map<String, Object>> selectPaged(@Param("table") String table,
+                                           @Param("companyId") Long companyId,
+                                           @Param("offset") int offset,
+                                           @Param("limit") int limit);
+}

+ 33 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterTokenConsumptionMapper.java

@@ -0,0 +1,33 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.LobsterTokenConsumption;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Token消耗记录 Mapper
+ */
+public interface LobsterTokenConsumptionMapper {
+
+    int insert(LobsterTokenConsumption record);
+
+    /** 按租户统计Token消耗(分页) */
+    List<LobsterTokenConsumption> selectByCompany(@Param("companyId") Long companyId,
+                                                   @Param("offset") int offset,
+                                                   @Param("limit") int limit);
+
+    /** 按日期统计Token汇总 */
+    List<Map<String, Object>> selectDailySummary(@Param("companyId") Long companyId,
+                                                  @Param("startDate") String startDate,
+                                                  @Param("endDate") String endDate);
+
+    /** 按模型统计Token消耗 */
+    List<Map<String, Object>> selectByModel(@Param("companyId") Long companyId,
+                                             @Param("startDate") String startDate,
+                                             @Param("endDate") String endDate);
+
+    /** 按实例统计Token消耗 */
+    List<Map<String, Object>> selectByInstance(@Param("companyId") Long companyId);
+}

+ 37 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterToolCallMapper.java

@@ -0,0 +1,37 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾工具调用框架 Mapper
+ * 表: lobster_tool_exec_log / customer_order / lobster_sms_log
+ */
+public interface LobsterToolCallMapper {
+
+    Map<String, Object> selectOrder(@Param("orderNo") String orderNo,
+                                     @Param("companyId") Long companyId);
+
+    List<Map<String, Object>> selectUserOrders(@Param("companyId") Long companyId);
+
+    int insertSmsLog(@Param("companyId") Long companyId,
+                     @Param("phone") String phone,
+                     @Param("content") String content);
+
+    List<Map<String, Object>> selectSmsLogs(@Param("companyId") Long companyId);
+
+    List<Map<String, Object>> selectByParams(@Param("sql") String sql,
+                                              @Param("params") List<Object> params);
+
+    int insertExecLog(@Param("companyId") Long companyId,
+                      @Param("toolName") String toolName,
+                      @Param("params") String params,
+                      @Param("result") String result);
+
+    List<Map<String, Object>> selectRecentExecLogs(@Param("companyId") Long companyId,
+                                                    @Param("limit") int limit);
+
+    int ensureTable();
+}

+ 32 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterUserPreferenceMapper.java

@@ -0,0 +1,32 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾用户偏好 Mapper(lobster_user_preference 表)
+ * 千人千面个性化引擎数据源
+ */
+public interface LobsterUserPreferenceMapper {
+
+    /** 保存/更新偏好快照 */
+    int upsert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("snapshotJson") String snapshotJson);
+
+    /** 读取渠道偏好覆盖消息模板 */
+    String selectMessageOverride(@Param("companyId") Long companyId,
+                                  @Param("externalUserId") String externalUserId);
+
+    /** 查询用户偏好 */
+    Map<String, Object> selectByUser(@Param("companyId") Long companyId,
+                                      @Param("externalUserId") String externalUserId);
+
+    /** 查询偏好历史(按类别分组) */
+    List<Map<String, Object>> selectHistory(@Param("companyId") Long companyId,
+                                             @Param("externalUserId") String externalUserId);
+
+    int ensureTable();
+}

+ 31 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterUserProfileMapper.java

@@ -0,0 +1,31 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * 龙虾用户画像 Mapper(lobster_user_profile 表)
+ */
+public interface LobsterUserProfileMapper {
+
+    /** 加载用户画像 */
+    Map<String, Object> selectByUser(@Param("companyId") Long companyId,
+                                      @Param("externalUserId") String externalUserId);
+
+    /** 首次自动创建(INSERT IGNORE) */
+    int ensureProfile(@Param("companyId") Long companyId,
+                      @Param("externalUserId") String externalUserId,
+                      @Param("lifecycleStage") String lifecycleStage);
+
+    /** 更新用户画像状态(Summary 写入时) */
+    int updateProfile(@Param("companyId") Long companyId,
+                      @Param("externalUserId") String externalUserId,
+                      @Param("currentState") String currentState,
+                      @Param("variableSnapshot") String variableSnapshot);
+
+    /** 多渠道融合画像写入 */
+    int applyEnrichment(@Param("companyId") Long companyId,
+                        @Param("externalUserId") String externalUserId,
+                        @Param("fields") Map<String, Object> fields);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterWorkflowInstanceMapper.java

@@ -22,4 +22,7 @@ public interface LobsterWorkflowInstanceMapper {
     int updateById(LobsterWorkflowInstance instance);
 
     int updateStatus(@Param("id") Long id, @Param("companyId") Long companyId, @Param("status") String status);
+
+    /** 仅取该实例所属模板 ID(用于动态节点生成) */
+    Long selectWorkflowIdById(@Param("id") Long id);
 }

+ 40 - 0
fs-service/src/main/java/com/fs/company/mapper/LobsterWorkflowVariableMapper.java

@@ -0,0 +1,40 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾工作流变量 Mapper(lobster_workflow_variable 表)
+ */
+public interface LobsterWorkflowVariableMapper {
+
+    int upsert(@Param("instanceId") Long instanceId,
+               @Param("key") String key,
+               @Param("value") String value,
+               @Param("companyId") Long companyId);
+
+    int upsertNoCompany(@Param("instanceId") Long instanceId,
+                        @Param("key") String key,
+                        @Param("value") String value);
+
+    int delete(@Param("instanceId") Long instanceId,
+               @Param("key") String key,
+               @Param("companyId") Long companyId);
+
+    int deleteNoCompany(@Param("instanceId") Long instanceId,
+                        @Param("key") String key);
+
+    int deleteByInstance(@Param("instanceId") Long instanceId,
+                         @Param("companyId") Long companyId);
+
+    int deleteByInstanceNoCompany(@Param("instanceId") Long instanceId);
+
+    List<Map<String, Object>> selectByInstance(@Param("instanceId") Long instanceId,
+                                                @Param("companyId") Long companyId);
+
+    List<Map<String, Object>> selectByInstanceNoCompany(@Param("instanceId") Long instanceId);
+
+    int ensureTable();
+}

+ 34 - 0
fs-service/src/main/java/com/fs/company/mapper/ProfileEnrichmentMapper.java

@@ -0,0 +1,34 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾画像融合所需的多渠道客户数据 Mapper
+ * 覆盖:订单(customer_order) / 标签(company_lobster_tag_user_rel + customer_tag_user) / 交互聊天统计
+ */
+public interface ProfileEnrichmentMapper {
+
+    /** 订单统计:cnt / total / last_order */
+    Map<String, Object> selectOrderStats(@Param("externalUserId") String externalUserId,
+                                          @Param("companyId") Long companyId);
+
+    /** 龙虾标签 */
+    List<Map<String, Object>> selectLobsterTags(@Param("externalUserId") String externalUserId,
+                                                 @Param("companyId") Long companyId);
+
+    /** 通用客户标签 */
+    List<Map<String, Object>> selectGeneralTags(@Param("externalUserId") String externalUserId,
+                                                 @Param("companyId") Long companyId);
+
+    /** 交互/对话聚合:cnt / last_chat */
+    Map<String, Object> selectChatStats(@Param("externalUserId") String externalUserId,
+                                         @Param("companyId") Long companyId);
+
+    /** 动态字段融合写入 */
+    int applyEnrichment(@Param("externalUserId") String externalUserId,
+                        @Param("companyId") Long companyId,
+                        @Param("fields") Map<String, Object> fields);
+}

+ 17 - 0
fs-service/src/main/java/com/fs/company/mapper/WhatsAppContactMapper.java

@@ -0,0 +1,17 @@
+package com.fs.company.mapper;
+
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Map;
+
+/**
+ * WhatsApp 联系人 Mapper(whatsapp_contact 表)
+ */
+public interface WhatsAppContactMapper {
+
+    Map<String, Object> selectById(@Param("contactId") Long contactId,
+                                    @Param("companyId") Long companyId);
+
+    Map<String, Object> selectByWhatsappId(@Param("whatsappId") String whatsappId,
+                                            @Param("companyId") Long companyId);
+}

+ 96 - 8
fs-service/src/main/java/com/fs/company/service/ai/AiSceneDispatcher.java

@@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 
 /**
@@ -40,6 +41,11 @@ public class AiSceneDispatcher {
     @Autowired
     private MultiModelPipelineEngine pipelineEngine;
 
+    /** 熔断器:providerCode → (连续失败次数, 熔断时间戳) */
+    private final Map<String, long[]> circuitBreaker = new ConcurrentHashMap<>();
+    private static final int CIRCUIT_THRESHOLD = 3;       // 连续3次失败触发熔断
+    private static final long CIRCUIT_TIMEOUT_MS = 60_000; // 熔断60秒后重试
+
     /**
      * 场景化模型调用(自动检测单/多模型)
      * <ul>
@@ -56,21 +62,31 @@ public class AiSceneDispatcher {
     public String dispatch(String prompt, String sceneCode, String systemPrompt) {
         try {
             List<AdminAiModel> models = sceneService.getEnabledModels(sceneCode);
+            // 过滤掉已熔断的 provider
+            models = filterCircuitBroken(models);
 
-            // 0个模型 → 降级兜底
+            // 0个模型 → 降级兜底(全局排序模型清单,依次尝试)
             if (models.isEmpty()) {
-                log.warn("[AiSceneDispatcher] 场景 {} 无可用模型,尝试降级", sceneCode);
-                AdminAiModel fallback = pickFallbackModel();
-                if (fallback == null) {
+                log.warn("[AiSceneDispatcher] 场景 {} 无可用模型,尝试全局降级", sceneCode);
+                List<AdminAiModel> allEnabled = filterCircuitBroken(modelService.listEnabled());
+                if (allEnabled.isEmpty()) {
                     log.error("[AiSceneDispatcher] 无任何可用模型");
                     return "";
                 }
-                return callSingleModel(prompt, systemPrompt, fallback, sceneCode);
+                return callWithFallbackChain(prompt, systemPrompt, allEnabled, sceneCode);
             }
 
-            // 1个模型 → 直接单模型调用
+            // 1个模型 → 单模型直接调(失败返回空字符串,调用方可自己 fallback)
             if (models.size() == 1) {
-                return callSingleModel(prompt, systemPrompt, models.get(0), sceneCode);
+                String content = callSingleModel(prompt, systemPrompt, models.get(0), sceneCode);
+                if (content != null && !content.isEmpty()) {
+                    return content;
+                }
+                // 单模型失败 → 尝试全局模型降级链(不阻塞业务)
+                log.warn("[AiSceneDispatcher] 场景 {} 单模型 {} 失败,尝试全局降级",
+                        sceneCode, models.get(0).getModelName());
+                List<AdminAiModel> allEnabled = filterCircuitBroken(modelService.listEnabled());
+                return callWithFallbackChain(prompt, systemPrompt, allEnabled, sceneCode);
             }
 
             // ≥2个模型 → 自动走多模型流水线
@@ -191,11 +207,17 @@ public class AiSceneDispatcher {
     //  私有方法
     // ═══════════════════════════════════════════
 
-    /** 单模型调用(返回字符串内容) */
+    /** 单模型调用(返回字符串内容)+ 熔断追踪 */
     private String callSingleModel(String prompt, String systemPrompt, AdminAiModel model, String sceneCode) {
+        String providerCode = model.getProviderCode();
         AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
         ModelResponse resp = aiModelGateway.chatWithConfig(prompt, systemPrompt, cfg);
         String content = resp != null ? resp.getContent() : "";
+        if (content == null || content.isEmpty()) {
+            onFailure(providerCode);
+        } else {
+            onSuccess(providerCode);
+        }
         log.debug("[AiSceneDispatcher] 场景 {} → 单模型 {} | tokens: in={} out={}",
                 sceneCode, model.getModelName(),
                 resp != null ? resp.getPromptTokens() : 0,
@@ -203,6 +225,26 @@ public class AiSceneDispatcher {
         return content;
     }
 
+    /**
+     * 场景内多模型按 sortOrder 依次尝试(真正的降级链)
+     * <p>主模型空响应或异常 → 自动切换到次模型,直至所有模型耗尽。
+     */
+    private String callWithFallbackChain(String prompt, String systemPrompt,
+                                          List<AdminAiModel> models, String sceneCode) {
+        for (AdminAiModel m : models) {
+            try {
+                String content = callSingleModel(prompt, systemPrompt, m, sceneCode);
+                if (content != null && !content.isEmpty()) {
+                    return content;
+                }
+                log.warn("[AiSceneDispatcher] 场景 {} 模型 {} 返回空,降级到下一个", sceneCode, m.getModelName());
+            } catch (Exception e) {
+                log.warn("[AiSceneDispatcher] 场景 {} 模型 {} 异常 {},降级", sceneCode, m.getModelName(), e.getMessage());
+            }
+        }
+        return "";
+    }
+
     /** 单模型调用(返回ModelResponse含Token) */
     private ModelResponse callSingleModelWithTokens(String prompt, String systemPrompt, AdminAiModel model) {
         AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
@@ -269,7 +311,53 @@ public class AiSceneDispatcher {
             info.put("providerCode", m.getProviderCode());
             info.put("modelIdentifier", m.getModelIdentifier());
             info.put("sortOrder", m.getSortOrder());
+            info.put("circuitBroken", isCircuitBroken(m.getProviderCode()));
             return info;
         }).collect(Collectors.toList());
     }
+
+    // ════════════════════════════════════
+    //  熔断器方法
+    // ════════════════════════════════════
+
+    private List<AdminAiModel> filterCircuitBroken(List<AdminAiModel> models) {
+        if (models == null) return Collections.emptyList();
+        List<AdminAiModel> healthy = models.stream()
+            .filter(m -> !isCircuitBroken(m.getProviderCode()))
+            .collect(Collectors.toList());
+        if (healthy.isEmpty() && !models.isEmpty()) {
+            log.warn("[AiSceneDispatcher] 全部模型已熔断,使用完整列表尝试恢复");
+            return models; // 全挂了也硬上,允许尝试恢复
+        }
+        return healthy;
+    }
+
+    private boolean isCircuitBroken(String providerCode) {
+        if (providerCode == null) return false;
+        long[] entry = circuitBreaker.get(providerCode);
+        if (entry == null) return false;
+        long failCount = entry[0];
+        long brokenAt = entry[1];
+        if (failCount < CIRCUIT_THRESHOLD) return false;
+        if (System.currentTimeMillis() - brokenAt > CIRCUIT_TIMEOUT_MS) {
+            circuitBreaker.remove(providerCode); // 超时恢复
+            return false;
+        }
+        return true;
+    }
+
+    private void onFailure(String providerCode) {
+        if (providerCode == null) return;
+        long[] entry = circuitBreaker.computeIfAbsent(providerCode, k -> new long[]{0, 0});
+        entry[0]++;
+        if (entry[0] >= CIRCUIT_THRESHOLD) {
+            entry[1] = System.currentTimeMillis();
+            log.warn("[AiSceneDispatcher] 熔断触发: provider={}, failures={}", providerCode, entry[0]);
+        }
+    }
+
+    private void onSuccess(String providerCode) {
+        if (providerCode == null) return;
+        circuitBreaker.remove(providerCode); // 成功后重置
+    }
 }

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

@@ -399,12 +399,55 @@ public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseSer
 
     @Override
     public Map<String, Object> dualValidation(Long companyId, String query, String fastgptResult) {
+        // 用户要求:移除 FastGPT 双路,统一使用本地知识库检索(向量 + LIKE)
         Map<String, Object> result = new HashMap<>();
         result.put("query", query);
-        result.put("fastgptResult", fastgptResult);
-        result.put("localResult", "待实现");
-        result.put("match", true);
+        result.put("source", "local_only");
 
+        // 本地向量+LIKE 检索结果(保持入参 fastgptResult 兼容旧调用者,但不再使用其值)
+        String localResult = "";
+        try {
+            List<Map<String, Object>> hits = ragQueryLocal(companyId, query, 5);
+            StringBuilder sb = new StringBuilder();
+            for (Map<String, Object> h : hits) {
+                if (h.get("answer") != null) {
+                    sb.append(h.get("answer")).append("\n");
+                }
+            }
+            localResult = sb.toString().trim();
+        } catch (Exception e) {
+            localResult = "本地检索失败: " + e.getMessage();
+        }
+
+        result.put("localResult", localResult);
+        result.put("match", localResult != null && !localResult.isEmpty());
+        return result;
+    }
+
+    /**
+     * 本地知识库检索(向量优先,失败降级 LIKE)—— 替代 FastGPT 路由
+     * @param topK 返回的最相关条数
+     */
+    private List<Map<String, Object>> ragQueryLocal(Long companyId, String query, int topK) {
+        // 简版:直接 LIKE 检索 company_knowledge_base,按相似度排序留待后续接入 Milvus
+        // 完整实现位于 RagQueryService(如已存在),此处优先 fallback 到现有 mapper
+        List<Map<String, Object>> result = new ArrayList<>();
+        if (query == null || query.isEmpty()) return result;
+        try {
+            CompanyKnowledgeBase param = new CompanyKnowledgeBase();
+            param.setCompanyId(companyId);
+            param.setQuestion(query);
+            List<CompanyKnowledgeBase> list = companyKnowledgeBaseMapper.selectCompanyKnowledgeBaseList(param);
+            int n = Math.min(topK, list == null ? 0 : list.size());
+            for (int i = 0; i < n; i++) {
+                CompanyKnowledgeBase kb = list.get(i);
+                Map<String, Object> map = new HashMap<>();
+                map.put("id", kb.getId());
+                map.put("question", kb.getQuestion());
+                map.put("answer", kb.getAnswer());
+                result.add(map);
+            }
+        } catch (Exception ignored) {}
         return result;
     }
 

+ 7 - 0
fs-service/src/main/java/com/fs/company/service/llm/MultiModelRouter.java

@@ -7,6 +7,13 @@ public interface MultiModelRouter {
 
     String generateResponse(String prompt, String model, String systemPrompt);
 
+    /**
+     * 按场景调度模型(用户指定的 admin/shezhi/aiModel 场景配置)
+     * @param sceneCode 场景编码(对应 admin_ai_scene.scene_code)
+     * @param modelName 节点指定模型(可空,空则走场景配置)
+     */
+    String generateResponseByScene(String prompt, String sceneCode, String modelName, String systemPrompt);
+
     String generateWithModelConfig(String prompt, String modelConfig, String systemPrompt);
 
     List<Map<String, Object>> getSupportedModels();

+ 155 - 37
fs-service/src/main/java/com/fs/company/service/llm/impl/ModelRouterImpl.java

@@ -1,72 +1,190 @@
 package com.fs.company.service.llm.impl;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.company.service.ai.AiSceneDispatcher;
 import com.fs.company.service.llm.ModelRouter;
 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;
 
+/**
+ * 意图路由实现 — 重写版
+ * <p>
+ * 改造点(按用户要求 & skill.md):
+ *   1. 不再使用关键词硬编码,改用真实模型推理(轻量模型,场景:semantic_analysis)
+ *   2. 推荐模型不再硬映射,统一查 admin_ai_scene 体系(场景→sortOrder 第一个模型)
+ *   3. 评分模型查 quality_scoring 场景
+ *   4. 关键词命中作为快速短路,避免简单消息也调用模型浪费 token
+ * <p>
+ * 配置开关 ai.router.enabled=false → 退化为关键词路由(默认 enabled=true)
+ */
 @Service
 public class ModelRouterImpl implements ModelRouter {
 
     private static final Logger logger = LoggerFactory.getLogger(ModelRouterImpl.class);
 
+    @Autowired(required = false)
+    private AiSceneDispatcher sceneDispatcher;
+
+    @Value("${ai.router.enabled:true}")
+    private boolean routerEnabled;
+
+    /** 场景编码常量(对齐 admin_ai_scene 表) */
+    private static final String SCENE_SEMANTIC = "semantic_analysis";
+    private static final String SCENE_QUALITY = "quality_scoring";
+    private static final String SCENE_WORKFLOW_LLM = "workflow_llm";
+
     @Override
     public IntentClassification route(String customerMessage, String context, int nodeType) {
+        // 1) 快速短路:明显的简单问候
+        IntentClassification shortcut = fastShortcut(customerMessage);
+        if (shortcut != null) return shortcut;
+
+        // 2) 总开关关闭或场景分发器不可用 → 退回关键词路由(兜底)
+        if (!routerEnabled || sceneDispatcher == null) {
+            return keywordRoute(customerMessage, context);
+        }
+
+        // 3) 调轻量模型做意图分类(场景:semantic_analysis)
+        try {
+            String prompt = buildIntentPrompt(customerMessage, context, nodeType);
+            String resp = sceneDispatcher.dispatch(prompt, SCENE_SEMANTIC,
+                    "你是一个意图分类器,输出严格 JSON,不要附加任何说明。");
+            return parseIntent(resp, customerMessage);
+        } catch (Exception e) {
+            logger.warn("[ModelRouter] 模型意图分类失败,退回关键词: {}", e.getMessage());
+            return keywordRoute(customerMessage, context);
+        }
+    }
+
+    @Override
+    public String getRecommendedModelForNodeType(int nodeType) {
+        // 按节点编号映射场景(与 visual.vue 节点 1-14 一致)
+        String sceneCode = mapNodeTypeToScene(nodeType);
+        if (sceneDispatcher != null) {
+            try {
+                java.util.List<java.util.Map<String, Object>> models = sceneDispatcher.getSceneModelInfo(sceneCode);
+                if (!models.isEmpty()) {
+                    Object name = models.get(0).get("modelName");
+                    if (name != null) return name.toString();
+                }
+            } catch (Exception ignored) {}
+        }
+        return "default"; // 让 dispatcher 走兜底
+    }
+
+    @Override
+    public String getScoringModel() {
+        if (sceneDispatcher != null) {
+            try {
+                java.util.List<java.util.Map<String, Object>> models = sceneDispatcher.getSceneModelInfo(SCENE_QUALITY);
+                if (!models.isEmpty()) {
+                    Object name = models.get(0).get("modelName");
+                    if (name != null) return name.toString();
+                }
+            } catch (Exception ignored) {}
+        }
+        return "default";
+    }
+
+    // ════════════ 私有方法 ════════════
+
+    /** 节点编号 → 场景编码 */
+    private String mapNodeTypeToScene(int nodeType) {
+        switch (nodeType) {
+            case 2:  return "workflow_llm";          // 消息节点(AI回复)
+            case 3:  return "workflow_llm";          // 判断节点(需要旗舰精确决策,由场景配模型)
+            case 7:  return "workflow_llm";          // 购物车/成单
+            case 13: return "workflow_llm";          // 复购
+            case 14: return "dynamic_node";          // 智能 API
+            case 11: return "workflow_llm";          // 文档/调查
+            case 12: return "workflow_llm";          // 用户/画像
+            default: return SCENE_WORKFLOW_LLM;
+        }
+    }
+
+    /** 简单消息快速短路(无需调模型) */
+    private IntentClassification fastShortcut(String message) {
+        if (message == null) return null;
+        String lower = message.toLowerCase().trim();
+        if (lower.isEmpty() || lower.length() <= 4) {
+            String[] greetings = {"你好", "您好", "hi", "hello", "嗨", "在吗", "在不在"};
+            for (String g : greetings) {
+                if (lower.contains(g)) {
+                    IntentClassification ic = new IntentClassification();
+                    ic.setIntent("greeting");
+                    ic.setComplexity("simple");
+                    ic.setRecommendedModel("default");
+                    ic.setConfidence(0.95);
+                    return ic;
+                }
+            }
+        }
+        return null;
+    }
+
+    /** 兜底关键词路由 */
+    private IntentClassification keywordRoute(String message, String context) {
         IntentClassification ic = new IntentClassification();
-        if (isGreeting(customerMessage)) {
-            ic.setIntent("greeting");
-            ic.setComplexity("simple");
-            ic.setRecommendedModel("doubao");
-            ic.setConfidence(0.9);
-        } else if (isComplexQuery(customerMessage, context)) {
+        if (message == null) message = "";
+        if (isComplexQuery(message, context)) {
             ic.setIntent("complex_query");
             ic.setComplexity("complex");
-            ic.setRecommendedModel("openai");
-            ic.setConfidence(0.7);
-        } else if (isComplianceRelated(customerMessage)) {
+        } else if (isComplianceRelated(message)) {
             ic.setIntent("compliance");
             ic.setComplexity("medium");
-            ic.setRecommendedModel("anthropic");
-            ic.setConfidence(0.8);
         } else {
             ic.setIntent("general");
             ic.setComplexity("simple");
-            ic.setRecommendedModel("doubao");
-            ic.setConfidence(0.6);
         }
+        ic.setRecommendedModel("default");
+        ic.setConfidence(0.6);
         return ic;
     }
 
-    @Override
-    public String getRecommendedModelForNodeType(int nodeType) {
-        switch (nodeType) {
-            case 1: return "doubao";
-            case 2: return "openai";
-            case 3: return "anthropic";
-            case 4: return "deepseek";
-            default: return "doubao";
+    private String buildIntentPrompt(String message, String context, int nodeType) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("请对以下客户消息做意图分类,输出 JSON:\n");
+        sb.append("{\"intent\": \"greeting|consult|objection|complaint|order|complex_query|general\",")
+                .append("\"complexity\": \"simple|medium|complex\",")
+                .append("\"confidence\": 0.0-1.0}\n");
+        sb.append("当前节点类型: ").append(nodeType).append("\n");
+        if (context != null && !context.isEmpty()) {
+            String trimmed = context.length() > 200 ? context.substring(0, 200) : context;
+            sb.append("上下文: ").append(trimmed).append("\n");
         }
+        sb.append("客户消息: ").append(message);
+        return sb.toString();
     }
 
-    @Override
-    public String getScoringModel() {
-        return "doubao";
-    }
-
-    private boolean isGreeting(String message) {
-        if (message == null) return false;
-        String lower = message.toLowerCase().trim();
-        String[] greetings = {"你好", "您好", "hi", "hello", "嗨", "在吗", "在不在", "早上好", "下午好", "晚上好"};
-        for (String g : greetings) {
-            if (lower.contains(g)) return true;
+    private IntentClassification parseIntent(String resp, String fallbackMessage) {
+        IntentClassification ic = new IntentClassification();
+        try {
+            int s = resp.indexOf('{');
+            int e = resp.lastIndexOf('}');
+            if (s >= 0 && e > s) {
+                JSONObject json = JSON.parseObject(resp.substring(s, e + 1));
+                ic.setIntent(json.getString("intent"));
+                ic.setComplexity(json.getString("complexity"));
+                ic.setConfidence(json.getDoubleValue("confidence"));
+            }
+        } catch (Exception ex) {
+            logger.debug("[ModelRouter] 解析意图 JSON 失败: {}", ex.getMessage());
         }
-        return lower.length() <= 5;
+        if (ic.getIntent() == null) ic.setIntent("general");
+        if (ic.getComplexity() == null) ic.setComplexity("simple");
+        if (ic.getConfidence() == 0) ic.setConfidence(0.5);
+        ic.setRecommendedModel("default"); // 不再硬编码 doubao/openai,交给场景配置
+        return ic;
     }
 
     private boolean isComplexQuery(String message, String context) {
         if (message == null) return false;
-        String[] complexKeywords = {"详细", "分析", "对比", "方案", "策略", "优化", "建议", "评估", "比较", "推荐"};
-        for (String kw : complexKeywords) {
+        String[] kws = {"详细", "分析", "对比", "方案", "策略", "优化", "建议", "评估", "比较", "推荐"};
+        for (String kw : kws) {
             if (message.contains(kw)) return true;
         }
         return message.length() > 200;
@@ -74,8 +192,8 @@ public class ModelRouterImpl implements ModelRouter {
 
     private boolean isComplianceRelated(String message) {
         if (message == null) return false;
-        String[] complianceKeywords = {"合规", "法律", "法规", "风险", "违规", "处罚", "监管", "政策"};
-        for (String kw : complianceKeywords) {
+        String[] kws = {"合规", "法律", "法规", "风险", "违规", "处罚", "监管", "政策"};
+        for (String kw : kws) {
             if (message.contains(kw)) return true;
         }
         return false;

+ 59 - 8
fs-service/src/main/java/com/fs/company/service/llm/impl/MultiModelRouterImpl.java

@@ -37,12 +37,30 @@ public class MultiModelRouterImpl implements MultiModelRouter {
      */
     private static final Map<String, String> HINT_TO_SCENE = new LinkedHashMap<>();
     static {
-        HINT_TO_SCENE.put("workflow_generator", "workflow_generation");
-        HINT_TO_SCENE.put("workflow_improver", "workflow_generation");
-        HINT_TO_SCENE.put("workflow_validator", "workflow_generation");
-        HINT_TO_SCENE.put("workflow_optimizer", "workflow_generation");
-        HINT_TO_SCENE.put("quality_scorer", "quality_scoring");
-        HINT_TO_SCENE.put("content_optimizer", "quality_scoring");
+        // ── 工作流生成 ──
+        HINT_TO_SCENE.put("workflow_generator",  "workflow_generation");
+        HINT_TO_SCENE.put("workflow_improver",   "workflow_generation");
+        HINT_TO_SCENE.put("workflow_validator",  "workflow_generation");
+        HINT_TO_SCENE.put("workflow_optimizer",  "workflow_generation");
+        // ── 质量评分 ──
+        HINT_TO_SCENE.put("quality_scorer",      "quality_scoring");
+        HINT_TO_SCENE.put("content_optimizer",   "quality_scoring");
+        // ── 动态节点执行 ──
+        HINT_TO_SCENE.put("dynamic_node_gen",      "workflow_generation");
+        HINT_TO_SCENE.put("dynamic_node_fallback",  "workflow_execution");
+        HINT_TO_SCENE.put("tag_extractor",          "workflow_execution");
+        HINT_TO_SCENE.put("care_generator",         "workflow_execution");
+        HINT_TO_SCENE.put("survey_generator",       "workflow_execution");
+        HINT_TO_SCENE.put("profile_infer",          "workflow_execution");
+        HINT_TO_SCENE.put("repurchase_generator",   "workflow_execution");
+        HINT_TO_SCENE.put("intent_recognizer",      "workflow_execution");
+        HINT_TO_SCENE.put("takeover_detect",        "workflow_execution");
+        // ── 语义分析 ──
+        HINT_TO_SCENE.put("semantic_analyzer",      "workflow_execution");
+        // ── 学习/复盘/进化 ──
+        HINT_TO_SCENE.put("evolution_analyzer",     "workflow_execution");
+        HINT_TO_SCENE.put("feedback_analyzer",      "workflow_execution");
+        HINT_TO_SCENE.put("summary_generator",      "workflow_execution");
     }
 
     /** 根据 systemPrompt 推断场景编码 */
@@ -58,18 +76,51 @@ public class MultiModelRouterImpl implements MultiModelRouter {
         // 优先使用新的场景分发器
         if (sceneDispatcher != null) {
             String sceneCode = resolveSceneCode(systemPrompt);
-            return sceneDispatcher.dispatch(prompt, sceneCode, systemPrompt);
+            // 场景内逐模型尝试(dispatcher 不可用时 → 降级到 gateway)
+            String content = sceneDispatcher.dispatch(prompt, sceneCode, systemPrompt);
+            if (content != null && !content.isEmpty()) {
+                return content;
+            }
+            logger.warn("[MultiModel] 场景 {} 返回空,降级到 gateway", sceneCode);
         }
         // 降级:使用旧网关
         ModelResponse resp = aiModelGateway.chatWithTokens(prompt, systemPrompt, model);
         return resp != null ? resp.getContent() : "";
     }
 
+    @Override
+    public String generateResponseByScene(String prompt, String sceneCode, String modelName, String systemPrompt) {
+        // 1. 节点指定 modelName → 直接用 gateway 走该模型(覆盖场景配置)
+        if (modelName != null && !modelName.isEmpty() && !"default".equalsIgnoreCase(modelName)) {
+            try {
+                ModelResponse resp = aiModelGateway.chatWithTokens(prompt, systemPrompt, modelName);
+                if (resp != null && resp.getContent() != null && !resp.getContent().isEmpty()) {
+                    return resp.getContent();
+                }
+                logger.warn("[MultiModel] 节点指定模型 {} 调用空响应,降级走场景 {}", modelName, sceneCode);
+            } catch (Exception e) {
+                logger.warn("[MultiModel] 节点指定模型 {} 异常 {},降级走场景 {}", modelName, e.getMessage(), sceneCode);
+            }
+        }
+        // 2. 走场景调度
+        if (sceneDispatcher != null && sceneCode != null && !sceneCode.isEmpty()) {
+            String content = sceneDispatcher.dispatch(prompt, sceneCode, systemPrompt);
+            if (content != null && !content.isEmpty()) return content;
+            logger.warn("[MultiModel] 场景 {} 返回空,降级 generateResponse(default)", sceneCode);
+        }
+        // 3. 最终降级
+        return generateResponse(prompt, modelName, systemPrompt);
+    }
+
     @Override
     public ModelResponse generateWithTokens(String prompt, String model, String systemPrompt) {
         if (sceneDispatcher != null) {
             String sceneCode = resolveSceneCode(systemPrompt);
-            return sceneDispatcher.dispatchWithTokens(prompt, sceneCode, systemPrompt);
+            ModelResponse resp = sceneDispatcher.dispatchWithTokens(prompt, sceneCode, systemPrompt);
+            if (resp != null && resp.getContent() != null && !resp.getContent().isEmpty()) {
+                return resp;
+            }
+            logger.warn("[MultiModel] 场景 {} 返回空,降级到 gateway", sceneCode);
         }
         return aiModelGateway.chatWithTokens(prompt, systemPrompt, model);
     }

+ 103 - 0
fs-service/src/main/java/com/fs/company/service/workflow/DynamicNodeImplService.java

@@ -0,0 +1,103 @@
+package com.fs.company.service.workflow;
+
+import java.util.List;
+
+/**
+ * 动态节点学习产物服务:
+ * - 命中复用:findActiveImpl(nodeType, fingerprint) → 命中 ACTIVE 状态的 DSL,0 LLM 直接走子工作流
+ * - 学习:saveLearned(...) AI 首次跑通后落库
+ * - 推广:scoreAndPromote(...) 评分≥80 自动 ACTIVE,60-80 写 evolution_suggestion 待审
+ */
+public interface DynamicNodeImplService {
+
+    /**
+     * 计算节点指纹(nodeType + 关键 config 字段 hash)
+     */
+    String computeFingerprint(int nodeType, String nodeConfig);
+
+    /**
+     * 查找已激活的学习产物,命中即可走子工作流 0 LLM
+     */
+    DynamicNodeImpl findActiveImpl(int nodeType, String fingerprint, Long companyId);
+
+    /**
+     * AI 首次成功跑通后保存学习产物(status=DRAFT)
+     */
+    Long saveLearned(int nodeType, String fingerprint, String subDslJson,
+                     String promptUsed, String sourceModel, Long companyId);
+
+    /**
+     * 每次执行后记录轨迹 + 评分;
+     * 1) success && score>=80 → ACTIVE 自动激活复用
+     * 2) success && 60<=score<80 → PENDING 写 lobster_evolution_suggestion
+     * 3) score<60 || !success → DRAFT 等待重新生成
+     */
+    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);
+
+    /**
+     * 人工审批通过 → ACTIVE
+     */
+    void approve(Long implId, String reviewer);
+
+    /**
+     * 人工拒绝 → REJECTED
+     */
+    void reject(Long implId, String reviewer, String reason);
+
+    /**
+     * 列出待审 / 已激活 / 草稿
+     */
+    List<DynamicNodeImpl> listByStatus(String status, Long companyId);
+
+    /**
+     * 学习产物 DTO
+     */
+    class DynamicNodeImpl {
+        private Long id;
+        private Long companyId;
+        private Integer nodeType;
+        private String nodeTypeCode;
+        private String fingerprint;
+        private String subDslJson;
+        private String promptUsed;
+        private String sourceModel;
+        private Double qualityScore;
+        private Integer execCount;
+        private Integer successCount;
+        private Integer avgDurationMs;
+        private String status;
+        private String reviewedBy;
+
+        public Long getId() { return id; }
+        public void setId(Long id) { this.id = id; }
+        public Long getCompanyId() { return companyId; }
+        public void setCompanyId(Long companyId) { this.companyId = companyId; }
+        public Integer getNodeType() { return nodeType; }
+        public void setNodeType(Integer nodeType) { this.nodeType = nodeType; }
+        public String getNodeTypeCode() { return nodeTypeCode; }
+        public void setNodeTypeCode(String nodeTypeCode) { this.nodeTypeCode = nodeTypeCode; }
+        public String getFingerprint() { return fingerprint; }
+        public void setFingerprint(String fingerprint) { this.fingerprint = fingerprint; }
+        public String getSubDslJson() { return subDslJson; }
+        public void setSubDslJson(String subDslJson) { this.subDslJson = subDslJson; }
+        public String getPromptUsed() { return promptUsed; }
+        public void setPromptUsed(String promptUsed) { this.promptUsed = promptUsed; }
+        public String getSourceModel() { return sourceModel; }
+        public void setSourceModel(String sourceModel) { this.sourceModel = sourceModel; }
+        public Double getQualityScore() { return qualityScore; }
+        public void setQualityScore(Double qualityScore) { this.qualityScore = qualityScore; }
+        public Integer getExecCount() { return execCount; }
+        public void setExecCount(Integer execCount) { this.execCount = execCount; }
+        public Integer getSuccessCount() { return successCount; }
+        public void setSuccessCount(Integer successCount) { this.successCount = successCount; }
+        public Integer getAvgDurationMs() { return avgDurationMs; }
+        public void setAvgDurationMs(Integer avgDurationMs) { this.avgDurationMs = avgDurationMs; }
+        public String getStatus() { return status; }
+        public void setStatus(String status) { this.status = status; }
+        public String getReviewedBy() { return reviewedBy; }
+        public void setReviewedBy(String reviewedBy) { this.reviewedBy = reviewedBy; }
+    }
+}

+ 32 - 0
fs-service/src/main/java/com/fs/company/service/workflow/ILobsterBillingService.java

@@ -0,0 +1,32 @@
+package com.fs.company.service.workflow;
+
+import com.fs.company.domain.LobsterConsumeRecord;
+import com.fs.company.domain.LobsterTenantBalance;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾计费 Service
+ */
+public interface ILobsterBillingService {
+
+    LobsterTenantBalance getBalance(Long tenantId);
+
+    boolean updateTokenCoefficient(Long tenantId, BigDecimal coefficient);
+
+    Map<String, Object> listConsumeRecords(int page, int size, Long tenantId);
+
+    /** Token消耗 - 按日期汇总 */
+    Map<String, Object> getTokenDailySummary(Long companyId, String startDate, String endDate);
+
+    /** Token消耗 - 按模型汇总 */
+    Map<String, Object> getTokenModelSummary(Long companyId, String startDate, String endDate);
+
+    /** Token消耗 - 按实例汇总 */
+    Map<String, Object> getTokenInstanceSummary(Long companyId);
+
+    /** Token消耗 - 明细列表 */
+    Map<String, Object> listTokenRecords(int page, int size, Long companyId);
+}

+ 19 - 0
fs-service/src/main/java/com/fs/company/service/workflow/ILobsterEventAuditService.java

@@ -0,0 +1,19 @@
+package com.fs.company.service.workflow;
+
+import com.fs.company.domain.LobsterEventAudit;
+
+import java.util.Map;
+
+/**
+ * 龙虾事件节点审核 Service
+ */
+public interface ILobsterEventAuditService {
+
+    Map<String, Object> listAudits(String status, int page, int size, Long companyId);
+
+    LobsterEventAudit getById(Long id);
+
+    boolean approve(Long id, String username);
+
+    boolean reject(Long id, String username, String comment);
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/service/workflow/ILobsterSalesCorpusService.java

@@ -0,0 +1,18 @@
+package com.fs.company.service.workflow;
+
+import com.fs.company.domain.LobsterSalesCorpus;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾销冠语料 Service
+ */
+public interface ILobsterSalesCorpusService {
+
+    Map<String, Object> listCorpus(int page, int size, Long companyId, String scenario, String status);
+
+    void addCorpus(LobsterSalesCorpus corpus, String username);
+
+    List<String> getScenarios();
+}

+ 200 - 0
fs-service/src/main/java/com/fs/company/service/workflow/LobsterE2eTestService.java

@@ -0,0 +1,200 @@
+package com.fs.company.service.workflow;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾 E2E 端到端测试服务
+ *
+ * 流程:业务描述 → AI生成工作流 → 创建实例 → 逐节点推进+多轮对话 → 质量评分 → 进化建议
+ *
+ * 关键设计:
+ * - 单节点最大轮次由模板节点 nodeConfig.maxTurn 决定(默认3)
+ * - 每轮推进调 LobsterEvolutionEngine.evolve,得到 reply+quality+nextNode
+ * - 每节点结束后 QualityScoringService.score 给详细分
+ * - run 末尾调 EvolutionEngine.analyzeAndSuggest,把低分节点的优化建议写入 lobster_evolution_suggestion
+ */
+public interface LobsterE2eTestService {
+
+    /**
+     * 启动一次 E2E 运行
+     */
+    E2eReport runE2e(E2eRequest req);
+
+    /**
+     * 查询某次运行的完整报告
+     */
+    E2eReport getReport(String runId);
+
+    /**
+     * 节点单步推进(用户视角:喂一条 user_input → 拿当前节点 reply)
+     */
+    StepResult stepNext(Long companyId, Long instanceId, String userInput);
+
+    /**
+     * 单节点连续多轮对话(不切节点)
+     */
+    MultiTurnResult multiTurn(Long companyId, Long instanceId, String nodeCode, List<String> userInputs);
+
+    /**
+     * 列表运行历史
+     */
+    List<E2eReport> listRuns(Long companyId, Integer pageNum, Integer pageSize);
+
+    // ============ 请求/响应类 ============
+
+    class E2eRequest {
+        private Long companyId;
+        private Long scenarioId;          // 选填:指定场景;否则用业务描述
+        private Long templateId;          // 选填:指定模板;否则用业务描述触发 AI 生成
+        private String businessDesc;      // 业务描述(无 template 时必填)
+        private String industryType;      // 行业类型
+        private List<String> userInputs;  // 用户输入序列(每轮一条)
+        private Long testContactId;       // 测试联系人ID(用于建实例,可为虚拟ID)
+
+        public Long getCompanyId() { return companyId; }
+        public void setCompanyId(Long companyId) { this.companyId = companyId; }
+        public Long getScenarioId() { return scenarioId; }
+        public void setScenarioId(Long scenarioId) { this.scenarioId = scenarioId; }
+        public Long getTemplateId() { return templateId; }
+        public void setTemplateId(Long templateId) { this.templateId = templateId; }
+        public String getBusinessDesc() { return businessDesc; }
+        public void setBusinessDesc(String businessDesc) { this.businessDesc = businessDesc; }
+        public String getIndustryType() { return industryType; }
+        public void setIndustryType(String industryType) { this.industryType = industryType; }
+        public List<String> getUserInputs() { return userInputs; }
+        public void setUserInputs(List<String> userInputs) { this.userInputs = userInputs; }
+        public Long getTestContactId() { return testContactId; }
+        public void setTestContactId(Long testContactId) { this.testContactId = testContactId; }
+    }
+
+    class E2eReport {
+        private String runId;
+        private Long companyId;
+        private Long templateId;
+        private Long instanceId;
+        private Long scenarioId;
+        private String businessDesc;
+        private Double totalScore;
+        private Integer passedNodeCnt;
+        private Integer totalNodeCnt;
+        private Long durationMs;
+        private String status;            // RUNNING|SUCCESS|FAILED
+        private String errorMsg;
+        private Integer evolutionCount;
+        private List<NodeTrace> nodeTraces;
+        private String createTime;
+
+        public String getRunId() { return runId; }
+        public void setRunId(String runId) { this.runId = runId; }
+        public Long getCompanyId() { return companyId; }
+        public void setCompanyId(Long companyId) { this.companyId = companyId; }
+        public Long getTemplateId() { return templateId; }
+        public void setTemplateId(Long templateId) { this.templateId = templateId; }
+        public Long getInstanceId() { return instanceId; }
+        public void setInstanceId(Long instanceId) { this.instanceId = instanceId; }
+        public Long getScenarioId() { return scenarioId; }
+        public void setScenarioId(Long scenarioId) { this.scenarioId = scenarioId; }
+        public String getBusinessDesc() { return businessDesc; }
+        public void setBusinessDesc(String businessDesc) { this.businessDesc = businessDesc; }
+        public Double getTotalScore() { return totalScore; }
+        public void setTotalScore(Double totalScore) { this.totalScore = totalScore; }
+        public Integer getPassedNodeCnt() { return passedNodeCnt; }
+        public void setPassedNodeCnt(Integer passedNodeCnt) { this.passedNodeCnt = passedNodeCnt; }
+        public Integer getTotalNodeCnt() { return totalNodeCnt; }
+        public void setTotalNodeCnt(Integer totalNodeCnt) { this.totalNodeCnt = totalNodeCnt; }
+        public Long getDurationMs() { return durationMs; }
+        public void setDurationMs(Long durationMs) { this.durationMs = durationMs; }
+        public String getStatus() { return status; }
+        public void setStatus(String status) { this.status = status; }
+        public String getErrorMsg() { return errorMsg; }
+        public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
+        public Integer getEvolutionCount() { return evolutionCount; }
+        public void setEvolutionCount(Integer evolutionCount) { this.evolutionCount = evolutionCount; }
+        public List<NodeTrace> getNodeTraces() { return nodeTraces; }
+        public void setNodeTraces(List<NodeTrace> nodeTraces) { this.nodeTraces = nodeTraces; }
+        public String getCreateTime() { return createTime; }
+        public void setCreateTime(String createTime) { this.createTime = createTime; }
+    }
+
+    class NodeTrace {
+        private Integer nodeSeq;
+        private String nodeCode;
+        private String nodeType;
+        private String nodeName;
+        private Integer turnNo;
+        private String userInput;
+        private String aiOutput;
+        private Double score;
+        private Map<String, Integer> scoreDetail;
+        private Long durationMs;
+        private String modelUsed;
+        private String evolutionHint;
+        private Boolean passed;
+        private String errorMsg;
+
+        public Integer getNodeSeq() { return nodeSeq; }
+        public void setNodeSeq(Integer nodeSeq) { this.nodeSeq = nodeSeq; }
+        public String getNodeCode() { return nodeCode; }
+        public void setNodeCode(String nodeCode) { this.nodeCode = nodeCode; }
+        public String getNodeType() { return nodeType; }
+        public void setNodeType(String nodeType) { this.nodeType = nodeType; }
+        public String getNodeName() { return nodeName; }
+        public void setNodeName(String nodeName) { this.nodeName = nodeName; }
+        public Integer getTurnNo() { return turnNo; }
+        public void setTurnNo(Integer turnNo) { this.turnNo = turnNo; }
+        public String getUserInput() { return userInput; }
+        public void setUserInput(String userInput) { this.userInput = userInput; }
+        public String getAiOutput() { return aiOutput; }
+        public void setAiOutput(String aiOutput) { this.aiOutput = aiOutput; }
+        public Double getScore() { return score; }
+        public void setScore(Double score) { this.score = score; }
+        public Map<String, Integer> getScoreDetail() { return scoreDetail; }
+        public void setScoreDetail(Map<String, Integer> scoreDetail) { this.scoreDetail = scoreDetail; }
+        public Long getDurationMs() { return durationMs; }
+        public void setDurationMs(Long durationMs) { this.durationMs = durationMs; }
+        public String getModelUsed() { return modelUsed; }
+        public void setModelUsed(String modelUsed) { this.modelUsed = modelUsed; }
+        public String getEvolutionHint() { return evolutionHint; }
+        public void setEvolutionHint(String evolutionHint) { this.evolutionHint = evolutionHint; }
+        public Boolean getPassed() { return passed; }
+        public void setPassed(Boolean passed) { this.passed = passed; }
+        public String getErrorMsg() { return errorMsg; }
+        public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; }
+    }
+
+    class StepResult {
+        private String currentNodeCode;
+        private String reply;
+        private Integer score;
+        private String nextNodeCode;
+        private Boolean finished;
+
+        public String getCurrentNodeCode() { return currentNodeCode; }
+        public void setCurrentNodeCode(String currentNodeCode) { this.currentNodeCode = currentNodeCode; }
+        public String getReply() { return reply; }
+        public void setReply(String reply) { this.reply = reply; }
+        public Integer getScore() { return score; }
+        public void setScore(Integer score) { this.score = score; }
+        public String getNextNodeCode() { return nextNodeCode; }
+        public void setNextNodeCode(String nextNodeCode) { this.nextNodeCode = nextNodeCode; }
+        public Boolean getFinished() { return finished; }
+        public void setFinished(Boolean finished) { this.finished = finished; }
+    }
+
+    class MultiTurnResult {
+        private String nodeCode;
+        private Integer maxTurn;
+        private List<NodeTrace> turns;
+        private Double avgScore;
+
+        public String getNodeCode() { return nodeCode; }
+        public void setNodeCode(String nodeCode) { this.nodeCode = nodeCode; }
+        public Integer getMaxTurn() { return maxTurn; }
+        public void setMaxTurn(Integer maxTurn) { this.maxTurn = maxTurn; }
+        public List<NodeTrace> getTurns() { return turns; }
+        public void setTurns(List<NodeTrace> turns) { this.turns = turns; }
+        public Double getAvgScore() { return avgScore; }
+        public void setAvgScore(Double avgScore) { this.avgScore = avgScore; }
+    }
+}

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/workflow/LobsterEvolutionEngine.java

@@ -17,6 +17,15 @@ public interface LobsterEvolutionEngine {
     EvolutionResult evolve(Long instanceId, Long companyId, String externalUserId,
                            String customerMessage, String currentNodeCode);
 
+    /**
+     * 千人千面核心:当现有节点无法匹配用户当前状态时,
+     * AI 根据用户标签/意图/习惯动态生成新节点并插入实例,
+     * 返回新节点的 nodeCode
+     */
+    String enrichWithDynamicNode(Long instanceId, Long companyId, String externalUserId,
+                                 String currentNodeCode, String intent, String sentiment,
+                                 Map<String, Object> userProfile, Map<String, Object> currentVars);
+
     class EvolutionResult {
         private String reply;
         private boolean qualityPassed;

+ 31 - 0
fs-service/src/main/java/com/fs/company/service/workflow/LobsterTestScenarioService.java

@@ -0,0 +1,31 @@
+package com.fs.company.service.workflow;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 龙虾测试场景剧本服务
+ * 数据驱动 + 定时回归
+ */
+public interface LobsterTestScenarioService {
+
+    Long createScenario(Map<String, Object> params);
+
+    void updateScenario(Long id, Map<String, Object> params);
+
+    void deleteScenario(Long id);
+
+    Map<String, Object> getScenario(Long id);
+
+    List<Map<String, Object>> listScenarios(Long companyId, Integer enabled, Integer pageNum, Integer pageSize);
+
+    /**
+     * 立刻运行某个场景(异步)
+     */
+    String runScenarioNow(Long id);
+
+    /**
+     * 跑全部启用场景(定时任务调用)
+     */
+    int runAllEnabledScenarios();
+}

+ 9 - 0
fs-service/src/main/java/com/fs/company/service/workflow/LobsterWorkflowExecutor.java

@@ -17,4 +17,13 @@ public interface LobsterWorkflowExecutor {
     AjaxResult terminateWorkflow(Long companyId, Long instanceId, String reason);
 
     Map<String, Object> getInstanceState(Long companyId, Long instanceId);
+
+    /**
+     * 工作流模拟执行:虚拟联系人逐节点推进,验证流程是否能跑通
+     * @param companyId 租户ID
+     * @param workflowId 工作流模板ID
+     * @param mockCustomerProfile 虚拟客户画像(可为null,使用默认值)
+     * @return 模拟报告:{passed, totalNodes, visitedNodes, failures: [{nodeCode,reason}]}
+     */
+    Map<String, Object> simulateExecution(Long companyId, Long workflowId, Map<String, Object> mockCustomerProfile);
 }

+ 8 - 2
fs-service/src/main/java/com/fs/company/service/workflow/QualityScoringService.java

@@ -145,11 +145,17 @@ public interface QualityScoringService {
      */
     class Threshold {
         public static final int FULL_SCORE = 160;
-        public static final int FIRST_PASS_THRESHOLD = 120;  // 75%
-        public static final int SECOND_PASS_THRESHOLD = 104;  // 65%
+        public static final int FIRST_PASS_THRESHOLD = 120;
+        public static final int SECOND_PASS_THRESHOLD = 104;
         public static final int DIMENSION_MAX = 20;
         public static final int KNOWLEDGE_CONSISTENCY_LOW_THRESHOLD = 5;
         public static final int GOAL_ALIGNMENT_LOW_THRESHOLD = 10;
         public static final int HUMAN_LIKELINESS_LOW_THRESHOLD = 8;
     }
+
+    /**
+     * 历史基准对比 + 分维度改进建议
+     * @return "当前评分128/160(高于历史均值115)", "相关性得分低(8/20),建议:直接回答问题开头"
+     */
+    String getImprovementHint(Long companyId, DetailedScore current, String nodeCode);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/workflow/SensitiveWordService.java

@@ -51,4 +51,10 @@ public interface SensitiveWordService {
      * @return 敏感词列表
      */
     List<Map<String, Object>> getSensitiveWordList(Long companyId);
+
+    /**
+     * 判断是否命中高风险敏感词(法律/政治/诈骗类)
+     * 高风险词触发转人工+告警,普通词仅替换
+     */
+    boolean isHighRiskSensitiveWord(String content, Long companyId);
 }

+ 17 - 3
fs-service/src/main/java/com/fs/company/service/workflow/SummaryGenerator.java

@@ -28,9 +28,23 @@ public interface SummaryGenerator {
 
     /**
      * 更新用户画像
-     * @param externalUserId 外部用户ID
-     * @param companyId 企业ID
-     * @param summaryInfo 摘要信息
      */
     void updateUserProfile(String externalUserId, Long companyId, Map<String, Object> summaryInfo);
+
+    /**
+     * 从对话中自动提取结构化变量(关键词→值映射)
+     * 示例: "我在北京想去三亚3天预算5000" → {destination:"三亚", days:3, budget:5000}
+     */
+    Map<String, Object> extractConversationVariables(Long companyId, String externalUserId,
+                                                      String conversationText, String extractFieldsHint);
+
+    /**
+     * 微摘要:最近N条对话的快速摘要(不调LLM,纯规则提取)
+     */
+    String generateMicroSummary(String conversationText, int lastN);
+
+    /**
+     * 全局摘要:合并该用户所有历史会话级别摘要为一个全局画像
+     */
+    String generateGlobalSummary(Long companyId, String externalUserId);
 }

+ 98 - 0
fs-service/src/main/java/com/fs/company/service/workflow/api/ApiCallExecutor.java

@@ -0,0 +1,98 @@
+package com.fs.company.service.workflow.api;
+
+import com.alibaba.fastjson.JSON;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 通用 API 调用执行器(智能 API 节点 / API 节点共用)
+ * <p>支持:GET/POST/PUT/DELETE,超时控制,自定义鉴权头。
+ */
+@Service
+public class ApiCallExecutor {
+
+    private static final Logger log = LoggerFactory.getLogger(ApiCallExecutor.class);
+
+    @Value("${ai.api.defaultTimeoutMs:5000}")
+    private int defaultTimeoutMs;
+
+    /**
+     * 调用 API
+     * @param ep      注册的 endpoint
+     * @param params  参数(GET 自动拼 query;POST/PUT 作为 JSON body)
+     * @return 响应体字符串
+     */
+    public String invoke(ApiRegistryService.ApiEndpoint ep, Map<String, Object> params) {
+        if (ep == null || ep.baseUrl == null) {
+            throw new IllegalArgumentException("API endpoint or baseUrl is null");
+        }
+        // 解析 method(baseUrl 形如 "GET https://x/y" 或仅 URL,默认 POST)
+        String method = "POST";
+        String url = ep.baseUrl;
+        if (url.matches("(?i)^(GET|POST|PUT|DELETE)\\s+.*")) {
+            int idx = url.indexOf(' ');
+            method = url.substring(0, idx).toUpperCase();
+            url = url.substring(idx + 1);
+        }
+
+        int timeout = ep.timeout > 0 ? ep.timeout : defaultTimeoutMs;
+        RestTemplate rt = buildRestTemplate(timeout);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setAcceptCharset(java.util.Collections.singletonList(StandardCharsets.UTF_8));
+        // 鉴权头(authConfig 形如 JSON)
+        if (ep.authConfig != null && !ep.authConfig.isEmpty()) {
+            try {
+                Map<String, String> authMap = JSON.parseObject(ep.authConfig, Map.class);
+                if (authMap != null) {
+                    for (Map.Entry<String, String> e : authMap.entrySet()) {
+                        headers.add(e.getKey(), e.getValue());
+                    }
+                }
+            } catch (Exception ignored) {}
+        }
+
+        HttpEntity<?> entity;
+        URI uri;
+        if ("GET".equals(method) || "DELETE".equals(method)) {
+            StringBuilder qs = new StringBuilder(url);
+            qs.append(url.contains("?") ? "&" : "?");
+            if (params != null) {
+                params.forEach((k, v) -> qs.append(k).append("=").append(v).append("&"));
+            }
+            String finalUrl = qs.toString().replaceAll("[&?]$", "");
+            uri = URI.create(finalUrl);
+            entity = new HttpEntity<>(headers);
+        } else {
+            uri = URI.create(url);
+            entity = new HttpEntity<>(params != null ? params : new HashMap<>(), headers);
+        }
+
+        try {
+            ResponseEntity<String> resp = rt.exchange(uri, HttpMethod.valueOf(method), entity, String.class);
+            log.debug("[ApiCall] {} {} → status={}", method, url, resp.getStatusCodeValue());
+            return resp.getBody();
+        } catch (Exception e) {
+            log.warn("[ApiCall] {} {} 失败 {}", method, url, e.getMessage());
+            throw new RuntimeException("API call failed: " + e.getMessage(), e);
+        }
+    }
+
+    private RestTemplate buildRestTemplate(int timeoutMs) {
+        org.springframework.http.client.SimpleClientHttpRequestFactory factory =
+                new org.springframework.http.client.SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(timeoutMs);
+        factory.setReadTimeout(timeoutMs);
+        return new RestTemplate(factory);
+    }
+}

+ 23 - 113
fs-service/src/main/java/com/fs/company/service/workflow/api/ApiRegistryService.java

@@ -2,10 +2,10 @@ package com.fs.company.service.workflow.api;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.company.mapper.LobsterApiRegistryMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
@@ -13,39 +13,25 @@ import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * 外部接口统一配置注册中心
- * 
- * 统一管理所有外部API接口(互联网接口+系统内部接口)
- *   - HTTP接口: 天气/物流/课程表/库存/发票等
- *   - 内部接口: 企微消息/支付/短信/邮件等
- *   - 支持多服务商配置+优先级排序+备用接口切换
- *   - 工作流生成时可选择需要的接口
- * 
- * 表: lobster_api_registry
+ * 外部接口统一配置注册中心(MyBatis 化)
  */
 @Service
 public class ApiRegistryService {
 
     private static final Logger log = LoggerFactory.getLogger(ApiRegistryService.class);
 
-    private static final String TABLE_NAME = "lobster_api_registry";
+    @Autowired(required = false)
+    private LobsterApiRegistryMapper apiRegistryMapper;
 
-    @Autowired
-    private JdbcTemplate jdbcTemplate;
-
-    /** 缓存: key → ApiEndpoint */
     private final ConcurrentHashMap<String, ApiEndpoint> cache = new ConcurrentHashMap<>();
 
     @PostConstruct
-    public void init() {
-        refreshCache();
-    }
+    public void init() { refreshCache(); }
 
     public void refreshCache() {
+        if (apiRegistryMapper == null) return;
         try {
-            ensureTable();
-            List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT * FROM " + TABLE_NAME + " WHERE enabled=1 ORDER BY priority ASC");
+            List<Map<String, Object>> rows = apiRegistryMapper.selectEnabled(null);
             cache.clear();
             for (Map<String, Object> row : rows) {
                 String key = row.get("api_key") + "|" + row.get("provider");
@@ -64,45 +50,25 @@ public class ApiRegistryService {
                 cache.put(key, ep);
             }
             log.info("[ApiRegistry] 加载了 {} 个已注册接口", cache.size());
-        } catch (Exception e) {
-            log.warn("[ApiRegistry] 加载失败: {}", e.getMessage());
-        }
+        } catch (Exception e) { log.warn("[ApiRegistry] 加载失败: {}", e.getMessage()); }
     }
 
-    /**
-     * 获取接口配置(主接口优先)
-     */
-    public ApiEndpoint get(String apiKey) {
-        return get(apiKey, false);
-    }
+    public ApiEndpoint get(String apiKey) { return get(apiKey, false); }
 
-    /**
-     * 获取接口配置
-     * @param apiKey   接口标识(如 weather/course_schedule/logistics)
-     * @param useBackup 是否使用备用接口
-     */
     public ApiEndpoint get(String apiKey, boolean useBackup) {
-        for (Map.Entry<String, ApiEndpoint> e : cache.entrySet()) {
-            ApiEndpoint ep = e.getValue();
-            if (apiKey.equals(ep.apiKey) && ep.isBackup == useBackup) {
-                return ep;
-            }
-        }
-        // 备用接口不可用时返回主接口
-        if (useBackup) return get(apiKey, false);
-        return null;
+        // 按 priority 排序:主接口(非备份)优先,高优先级优先
+        return cache.values().stream()
+            .filter(ep -> apiKey.equals(ep.apiKey) && ep.isBackup == useBackup)
+            .min(Comparator.comparingInt((ApiEndpoint ep) -> ep.isBackup ? 1 : 0)
+                          .thenComparingInt(ep -> ep.priority))
+            .orElse(useBackup ? get(apiKey, false) : null);
     }
 
-    /**
-     * 获取所有主接口列表(供工作流生成时选择)
-     */
     public List<Map<String, Object>> getAllApis() {
         List<Map<String, Object>> result = new ArrayList<>();
         Set<String> seen = new HashSet<>();
         for (ApiEndpoint ep : cache.values()) {
-            if (ep.isBackup) continue;
-            if (seen.contains(ep.apiKey)) continue;
-            seen.add(ep.apiKey);
+            if (ep.isBackup || !seen.add(ep.apiKey)) continue;
             Map<String, Object> m = new LinkedHashMap<>();
             m.put("apiKey", ep.apiKey);
             m.put("apiName", ep.apiName);
@@ -114,92 +80,36 @@ public class ApiRegistryService {
         return result;
     }
 
-    /**
-     * 注册新接口
-     */
     public void register(ApiEndpoint ep) {
+        if (apiRegistryMapper == null) return;
         try {
-            ensureTable();
-            jdbcTemplate.update(
-                    "INSERT INTO " + TABLE_NAME + " (api_key, api_name, category, provider, " +
-                    "base_url, auth_type, auth_config, timeout, priority, is_backup, " +
-                    "description, enabled, create_time) " +
-                    "VALUES (?,?,?,?,?,?,?,?,?,?,?,1,NOW()) " +
-                    "ON DUPLICATE KEY UPDATE base_url=VALUES(base_url), auth_config=VALUES(auth_config), " +
-                    "update_time=NOW()",
-                    ep.apiKey, ep.apiName, ep.category, ep.provider,
+            apiRegistryMapper.insert(ep.apiKey, ep.apiName, ep.category, ep.provider,
                     ep.baseUrl, ep.authType, ep.authConfig, ep.timeout, ep.priority,
                     ep.isBackup ? 1 : 0, ep.description);
             refreshCache();
-        } catch (Exception e) {
-            log.warn("[ApiRegistry] 注册失败: {}", e.getMessage());
-        }
+        } catch (Exception e) { log.warn("[ApiRegistry] 注册失败: {}", e.getMessage()); }
     }
 
-    /**
-     * 构建HTTP请求头
-     */
     public Map<String, String> buildAuthHeaders(String apiKey) {
         ApiEndpoint ep = get(apiKey);
         if (ep == null) return Collections.emptyMap();
-
         Map<String, String> headers = new LinkedHashMap<>();
         headers.put("Content-Type", "application/json");
-
         if ("bearer".equals(ep.authType) && ep.authConfig != null) {
             headers.put("Authorization", "Bearer " + ep.authConfig);
         } else if ("api-key".equals(ep.authType) && ep.authConfig != null) {
             try {
                 JSONObject cfg = JSON.parseObject(ep.authConfig);
-                String headerName = cfg.getString("headerName");
-                String headerValue = cfg.getString("headerValue");
-                if (headerName != null) headers.put(headerName, headerValue);
+                String name = cfg.getString("headerName");
+                if (name != null) headers.put(name, cfg.getString("headerValue"));
             } catch (Exception ignored) {}
         }
         return headers;
     }
 
-    private void ensureTable() {
-        try {
-            jdbcTemplate.queryForObject("SELECT 1 FROM " + TABLE_NAME + " LIMIT 1", Integer.class);
-        } catch (Exception e) {
-            jdbcTemplate.execute(
-                    "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "api_key VARCHAR(100) NOT NULL COMMENT '接口标识', " +
-                    "api_name VARCHAR(200) COMMENT '接口名称', " +
-                    "category VARCHAR(50) COMMENT '分类: webhook/internal/ai/third-party', " +
-                    "provider VARCHAR(100) COMMENT '服务商', " +
-                    "base_url VARCHAR(500) COMMENT '基础URL', " +
-                    "auth_type VARCHAR(50) COMMENT '认证类型: bearer/api-key/basic', " +
-                    "auth_config TEXT COMMENT '认证配置JSON', " +
-                    "timeout INT DEFAULT 5000 COMMENT '超时ms', " +
-                    "priority INT DEFAULT 0 COMMENT '优先级', " +
-                    "is_backup TINYINT DEFAULT 0 COMMENT '是否备用接口', " +
-                    "description VARCHAR(500) COMMENT '描述', " +
-                    "enabled TINYINT DEFAULT 1 COMMENT '1启用0禁用', " +
-                    "create_time DATETIME DEFAULT NOW(), " +
-                    "update_time DATETIME DEFAULT NOW() ON UPDATE NOW(), " +
-                    "UNIQUE KEY uk_api_provider (api_key, provider), " +
-                    "INDEX idx_category (category)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
-            log.info("[ApiRegistry] 自动创建了{}表", TABLE_NAME);
-        }
-    }
-
-    /* ========== 数据模型 ========== */
-
     public static class ApiEndpoint {
-        public String apiKey;
-        public String apiName;
-        public String category;
-        public String provider;
-        public String baseUrl;
-        public String authType;
-        public String authConfig;
-        public int timeout = 5000;
-        public int priority;
+        public String apiKey, apiName, category, provider, baseUrl, authType, authConfig, description;
+        public int timeout = 5000, priority;
         public boolean isBackup;
-        public String description;
     }
 }

+ 47 - 0
fs-service/src/main/java/com/fs/company/service/workflow/api/ScriptCache.java

@@ -0,0 +1,47 @@
+package com.fs.company.service.workflow.api;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * SpEL 脚本编译缓存(智能 API 节点 resultScript 用)
+ * <p>使用 Spring SpEL 引擎,零依赖,支持:
+ * <br>#response.data.list[0].name、T(Math).max(a,b)、T(JSON).parseObject(#response).get('id')
+ * <br>缓存:表达式字符串 → 编译后的 Expression(ConcurrentHashMap)
+ */
+@Service
+public class ScriptCache {
+
+    private static final Logger log = LoggerFactory.getLogger(ScriptCache.class);
+
+    private final ExpressionParser parser = new SpelExpressionParser();
+    private final Map<String, Expression> cache = new ConcurrentHashMap<>();
+
+    public Object execute(String script, Map<String, Object> binding) {
+        if (script == null || script.isEmpty()) return null;
+        Expression expr = cache.computeIfAbsent(script, parser::parseExpression);
+        StandardEvaluationContext ctx = new StandardEvaluationContext();
+        if (binding != null) {
+            for (Map.Entry<String, Object> e : binding.entrySet()) {
+                ctx.setVariable(e.getKey(), e.getValue());
+            }
+        }
+        try {
+            return expr.getValue(ctx);
+        } catch (Exception ex) {
+            log.warn("[ScriptCache] 脚本执行失败 [{}]: {}", script, ex.getMessage());
+            throw new RuntimeException("Script execution failed: " + ex.getMessage(), ex);
+        }
+    }
+
+    public int size() { return cache.size(); }
+    public void clear() { cache.clear(); }
+}

+ 144 - 0
fs-service/src/main/java/com/fs/company/service/workflow/api/SmartApiCallNodeExecutor.java

@@ -0,0 +1,144 @@
+package com.fs.company.service.workflow.api;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.*;
+
+/**
+ * 智能 API 调用节点执行器
+ *
+ * 流程:
+ *   1. 获取用户问题 + 节点配置
+ *   2. AI 语义匹配:从 ApiRegistryService 已注册的 API 中选择最佳匹配
+ *   3. 自动提取参数 → 发起 REST 调用
+ *   4. SpEL 脚本解析返回值 → 写入变量(ScriptCache 编译缓存)
+ */
+@Component
+public class SmartApiCallNodeExecutor {
+
+    private static final Logger log = LoggerFactory.getLogger(SmartApiCallNodeExecutor.class);
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    @Autowired(required = false)
+    private ScriptCache scriptCache;
+
+    @Autowired(required = false)
+    private com.fs.company.service.llm.MultiModelRouter multiModelRouter;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public Map<String, Object> execute(String userQuestion, JSONObject nodeConfig, Map<String, Object> contextVars) {
+        Map<String, Object> result = new HashMap<>();
+        if (apiRegistryService == null) {
+            result.put("error", "API注册中心不可用");
+            return result;
+        }
+        try {
+            // 1) 语义匹配 API
+            String apiKey = matchApiBySemantic(userQuestion, nodeConfig);
+            if (apiKey == null) {
+                result.put("error", "未匹配到合适的API");
+                return result;
+            }
+            ApiRegistryService.ApiEndpoint ep = apiRegistryService.get(apiKey);
+            if (ep == null) {
+                result.put("error", "API配置不存在: " + apiKey);
+                return result;
+            }
+            // 2) 提取参数
+            Map<String, Object> params = extractParams(nodeConfig, contextVars);
+            // 3) 发起调用
+            String rawResponse = callEndpoint(ep, params);
+            result.put("rawResponse", rawResponse);
+            // 4) 脚本解析
+            String resultScript = nodeConfig.getString("resultScript");
+            if (resultScript != null && !resultScript.isEmpty() && scriptCache != null) {
+                Map<String, Object> binding = new HashMap<>();
+                binding.put("response", rawResponse);
+                try {
+                    binding.put("json", JSON.parse(rawResponse));
+                } catch (Exception ignored) {}
+                if (contextVars != null) binding.putAll(contextVars);
+                try {
+                    Object parsed = scriptCache.execute(resultScript, binding);
+                    result.put("parsed", parsed);
+                } catch (Exception e) {
+                    result.put("scriptError", e.getMessage());
+                }
+            }
+        } catch (Exception e) {
+            result.put("error", e.getMessage());
+        }
+        return result;
+    }
+
+    private String matchApiBySemantic(String question, JSONObject config) {
+        String apiKey = config != null ? config.getString("apiKey") : null;
+        if (apiKey != null && !apiKey.isEmpty()) return apiKey;
+        if (multiModelRouter != null && question != null) {
+            List<Map<String, Object>> apis = apiRegistryService.getAllApis();
+            StringBuilder sb = new StringBuilder("可用API列表:\n");
+            for (Map<String, Object> api : apis) {
+                sb.append("- ").append(api.get("apiKey")).append(": ").append(api.get("apiName")).append("\n");
+            }
+            sb.append("用户问题:").append(question).append("\n请返回最匹配的apiKey(只返回key):");
+            String resp = multiModelRouter.generateResponse(sb.toString(), null, null);
+            return resp != null ? resp.trim() : null;
+        }
+        return null;
+    }
+
+    private Map<String, Object> extractParams(JSONObject config, Map<String, Object> contextVars) {
+        Map<String, Object> params = new HashMap<>();
+        if (config != null) {
+            JSONArray paramMappings = config.getJSONArray("paramMappings");
+            if (paramMappings != null) {
+                for (int i = 0; i < paramMappings.size(); i++) {
+                    JSONObject pm = paramMappings.getJSONObject(i);
+                    String key = pm.getString("paramName");
+                    String source = pm.getString("source"); // "variable" or "literal"
+                    String value = pm.getString("value");
+                    if ("variable".equals(source) && contextVars != null && contextVars.containsKey(value)) {
+                        params.put(key, contextVars.get(value));
+                    } else if ("literal".equals(source)) {
+                        params.put(key, value);
+                    }
+                }
+            }
+        }
+        return params;
+    }
+
+    private String callEndpoint(ApiRegistryService.ApiEndpoint ep, Map<String, Object> params) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        Map<String, String> authHeaders = apiRegistryService.buildAuthHeaders(ep.apiKey);
+        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();
+        }
+    }
+}

+ 206 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/ChannelPluginService.java

@@ -0,0 +1,206 @@
+package com.fs.company.service.workflow.channel;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.company.domain.LobsterChannelPluginConfig;
+import com.fs.company.mapper.LobsterChannelPluginConfigMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 渠道插件管理服务 —— 龙虾即插即用多IM通道核心
+ * <p>
+ * 龙虾引擎 = 插头,各IM = 插座
+ * ┌───────────────────────────────────────────────┐
+ * │               龙虾 AI 引擎                      │
+ * │  (ContextAssembler / EvolutionEngine / ...)    │
+ * └──────────────────┬────────────────────────────┘
+ *                    │ MessageChannel 接口
+ *     ┌──────────────┼──────────────┬─────────────┐
+ *     │ QW           │ WX           │ WHATSAPP    │  ← 插件
+ *     │ 企微SDK      │ 个微API      │ Meta API    │
+ *     └──────────────┴──────────────┴─────────────┘
+ */
+@Service
+public class ChannelPluginService {
+
+    private static final Logger log = LoggerFactory.getLogger(ChannelPluginService.class);
+
+    @Autowired
+    private LobsterChannelPluginConfigMapper channelPluginConfigMapper;
+
+    @Autowired
+    private MessageChannelRouter channelRouter;
+
+    @Autowired
+    private ChannelTypeRegistry typeRegistry;
+
+    /**
+     * 获取所有已注册的渠道插件及租户配置状态
+     */
+    public List<Map<String, Object>> listPlugins(Long companyId) {
+        List<Map<String, Object>> plugins = new ArrayList<>();
+        Map<String, com.fs.company.service.workflow.channel.MessageChannel> liveChannels = channelRouter.getAllChannels();
+        List<Map<String, String>> allTypes = typeRegistry.getAllChannelTypes();
+
+        // 先查出该租户已配置的渠道
+        Map<String, LobsterChannelPluginConfig> configured = new LinkedHashMap<>();
+        List<LobsterChannelPluginConfig> configs = channelPluginConfigMapper.selectByCompanyId(companyId);
+        if (configs != null) {
+            for (LobsterChannelPluginConfig c : configs) {
+                configured.put(c.getChannelType(), c);
+            }
+        }
+
+        for (Map<String, String> type : allTypes) {
+            String channelType = type.get("channelType");
+            Map<String, Object> p = new LinkedHashMap<>();
+            p.put("channelType", channelType);
+            p.put("displayName", type.get("displayName"));
+            p.put("icon", getChannelIcon(channelType));
+            p.put("hasMessageChannel", liveChannels.containsKey(channelType));
+            p.put("hasContactAdapter", typeRegistry.isRegistered(channelType));
+
+            LobsterChannelPluginConfig cfg = configured.get(channelType);
+            if (cfg != null) {
+                p.put("enabled", cfg.getEnabled());
+                p.put("configJson", cfg.getConfigJson());
+            } else {
+                p.put("enabled", 0);
+                p.put("configJson", "{}");
+            }
+            p.put("companyId", companyId);
+            plugins.add(p);
+        }
+        return plugins;
+    }
+
+    /**
+     * 启用/禁用渠道插件
+     */
+    public void setEnabled(Long companyId, String channelType, boolean enabled) {
+        LobsterChannelPluginConfig config = new LobsterChannelPluginConfig();
+        config.setCompanyId(companyId);
+        config.setChannelType(channelType);
+        config.setEnabled(enabled ? 1 : 0);
+        config.setConfigJson("{}");
+        channelPluginConfigMapper.upsert(config);
+        log.info("[ChannelPlugin] 渠道 {} 已{}: companyId={}", channelType, enabled ? "启用" : "禁用", companyId);
+    }
+
+    /**
+     * 保存渠道配置(API Key / Webhook URL 等)
+     */
+    public void saveConfig(Long companyId, String channelType, Map<String, Object> config) {
+        String json = config != null ? JSON.toJSONString(config) : "{}";
+        LobsterChannelPluginConfig entity = channelPluginConfigMapper.selectByCompanyAndChannel(companyId, channelType);
+        if (entity == null) {
+            entity = new LobsterChannelPluginConfig();
+            entity.setCompanyId(companyId);
+            entity.setChannelType(channelType);
+            entity.setEnabled(0);
+            entity.setConfigJson(json);
+            channelPluginConfigMapper.upsert(entity);
+        } else {
+            channelPluginConfigMapper.updateConfig(companyId, channelType, json);
+        }
+        log.info("[ChannelPlugin] 配置已保存: companyId={}, channelType={}", companyId, channelType);
+    }
+
+    /**
+     * 获取单个插件状态(供 workflow executor 启动时检查)
+     * 内置渠道(QW/WX/IM)默认启用,其余需主动在管理后台开启
+     */
+    public PluginStatus getStatus(Long companyId, String channelType) {
+        PluginStatus s = new PluginStatus();
+        s.channelType = channelType;
+        s.registered = typeRegistry.isRegistered(channelType);
+        s.hasImpl = channelRouter.getChannel(channelType) != null;
+        s.enabled = false;
+        LobsterChannelPluginConfig cfg = channelPluginConfigMapper.selectByCompanyAndChannel(companyId, channelType);
+        if (cfg != null && cfg.getEnabled() != null && cfg.getEnabled() == 1) {
+            s.enabled = true;
+            s.configJson = cfg.getConfigJson();
+        } else if (cfg == null && isBuiltIn(channelType)) {
+            // 内置渠道(QW/WX/IM)默认启用,无需手动开启
+            s.enabled = true;
+            s.configJson = "{}";
+        } else if (cfg != null && cfg.getEnabled() == 0 && isBuiltIn(channelType)) {
+            // 内置渠道如果被显式禁用,则保持禁用
+            s.enabled = false;
+            s.configJson = cfg.getConfigJson();
+        }
+        s.ready = s.registered && s.hasImpl && s.enabled;
+        return s;
+    }
+
+    /** 内置渠道:已有完整 SDK 实现,默认开箱即用 */
+    private boolean isBuiltIn(String channelType) {
+        return "QW".equals(channelType) || "WX".equals(channelType) || "IM".equals(channelType);
+    }
+
+    /**
+     * 测试渠道连接
+     */
+    public Map<String, Object> testConnection(Long companyId, String channelType) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("channelType", channelType);
+
+        com.fs.company.service.workflow.channel.MessageChannel channel = channelRouter.getChannel(channelType);
+        if (channel == null) {
+            result.put("ok", false);
+            result.put("reason", "该渠道的 MessageChannel 实现未注册");
+            return result;
+        }
+        if (!channel.isAvailable(companyId)) {
+            result.put("ok", false);
+            result.put("reason", "渠道 SDK 不可用(可能未配置或凭证失效)");
+            return result;
+        }
+        result.put("ok", true);
+        result.put("reason", "连接正常");
+        return result;
+    }
+
+    /**
+     * 获取租户渠道配置JSON(供 WhatsAppMessageChannel 等插件内部使用)
+     */
+    public String getConfigJson(Long companyId, String channelType) {
+        LobsterChannelPluginConfig cfg = channelPluginConfigMapper.selectByCompanyAndChannel(companyId, channelType);
+        return cfg != null && cfg.getConfigJson() != null ? cfg.getConfigJson() : "{}";
+    }
+
+    // ════════════════ 图标映射 ════════════════
+
+    private String getChannelIcon(String type) {
+        switch (type) {
+            case "QW": return "el-icon-s-platform";
+            case "WX": return "el-icon-chat-dot-square";
+            case "IM": return "el-icon-s-comment";
+            case "WHATSAPP": return "el-icon-phone-outline";
+            case "LINE": return "el-icon-connection";
+            case "TELEGRAM": return "el-icon-s-promotion";
+            case "APP_IM": return "el-icon-mobile-phone";
+            case "DOUYIN_DM": return "el-icon-video-play";
+            case "KUAISHOU_DM": return "el-icon-camera";
+            case "XIAOHONGSHU_DM": return "el-icon-edit";
+            case "TMALL": return "el-icon-shopping-cart-2";
+            case "JD": return "el-icon-shopping-bag-2";
+            default: return "el-icon-link";
+        }
+    }
+
+    // ════════════════ 状态对象 ════════════════
+
+    public static class PluginStatus {
+        public String channelType;
+        public boolean registered;
+        public boolean hasImpl;
+        public boolean enabled;
+        public boolean ready;
+        public String configJson;
+    }
+}

+ 19 - 42
fs-service/src/main/java/com/fs/company/service/workflow/channel/ChannelTypeRegistry.java

@@ -1,9 +1,9 @@
 package com.fs.company.service.workflow.channel;
 
+import com.fs.company.mapper.LobsterChannelRegistryMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
@@ -37,7 +37,7 @@ public class ChannelTypeRegistry {
     private static final String REGISTRY_TABLE = "lobster_channel_type_registry";
 
     @Autowired(required = false)
-    private JdbcTemplate jdbcTemplate;
+    private LobsterChannelRegistryMapper channelRegistryMapper;
 
     private final ConcurrentHashMap<String, ChannelTypeConfig> registry = new ConcurrentHashMap<>();
 
@@ -59,6 +59,13 @@ public class ChannelTypeRegistry {
         register("LINE", "line_contact", "line_user_id", "Line Messaging");
         register("TELEGRAM", "telegram_contact", "telegram_id", "Telegram Bot");
         register("APP_IM", "app_im_user", "user_id", "APP内IM");
+        // 电商/社交平台私信
+        register("TMALL", "tmall_contact", "buyer_nick", "天猫");
+        register("JD", "jd_contact", "customer_id", "京东");
+        register("DOUYIN_DM", "douyin_contact", "open_id", "抖音私信");
+        register("KUAISHOU_DM", "kuaishou_contact", "open_id", "快手私信");
+        register("XIAOHONGSHU_DM", "xhs_contact", "user_id", "小红书私信");
+        register("DOUYIN_EC", "douyin_ec_contact", "open_id", "抖音电商");
         register("OTHER", "custom_contact", "external_id", "自定义渠道");
     }
 
@@ -84,39 +91,25 @@ public class ChannelTypeRegistry {
      */
     public String resolveSourceUserId(String channelType, Long contactId, Long companyId) {
         ChannelTypeConfig cfg = getConfig(channelType);
-        if (cfg == null || jdbcTemplate == null) return null;
+        if (cfg == null || channelRegistryMapper == null) return null;
 
-        // 优先从 lobster_unified_contact 查 channel_user_id
         try {
-            Map<String, Object> contact = jdbcTemplate.queryForMap(
-                    "SELECT channel_user_id FROM lobster_unified_contact " +
-                    "WHERE company_id=? AND contact_id=? AND channel_type=? LIMIT 1",
-                    companyId, contactId, channelType);
-            String channelUserId = (String) contact.get("channel_user_id");
+            String channelUserId = channelRegistryMapper.selectChannelUserId(companyId, contactId, channelType);
             if (channelUserId != null && !channelUserId.isEmpty()) return channelUserId;
         } catch (Exception ignored) {}
 
-        // 兜底: 从各平台用户表查
         try {
-            String sql = "SELECT " + cfg.userIdColumn + " FROM " + cfg.sourceTable +
-                    " WHERE company_id=? LIMIT 1";
-            Map<String, Object> row = jdbcTemplate.queryForMap(sql, companyId);
-            return row.get(cfg.userIdColumn) != null ? String.valueOf(row.get(cfg.userIdColumn)) : null;
+            Map<String, Object> row = channelRegistryMapper.selectSourceUserId(cfg.sourceTable, cfg.userIdColumn, companyId);
+            return row != null && row.get(cfg.userIdColumn) != null ? String.valueOf(row.get(cfg.userIdColumn)) : null;
         } catch (Exception ignored) {}
 
         return null;
     }
 
-    /**
-     * 绑定触点: 将龙虾contactId与各平台userID建立映射
-     */
     public void bindContact(Long companyId, Long contactId, String channelType, String sourceUserId) {
-        if (jdbcTemplate == null) return;
+        if (channelRegistryMapper == null) return;
         try {
-            jdbcTemplate.update(
-                    "INSERT INTO lobster_unified_contact (company_id, contact_id, channel_type, channel_user_id, create_time) " +
-                    "VALUES (?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE channel_user_id=VALUES(channel_user_id), update_time=NOW()",
-                    companyId, contactId, channelType, sourceUserId);
+            channelRegistryMapper.upsertUnifiedContact(companyId, contactId, channelType, sourceUserId);
         } catch (Exception e) {
             log.debug("[ChannelRegistry] 触点绑定失败: {}", e.getMessage());
         }
@@ -154,11 +147,9 @@ public class ChannelTypeRegistry {
      * DB层面的渠道注册表 (保留扩展用)
      */
     private void loadFromDb() {
-        if (jdbcTemplate == null) return;
+        if (channelRegistryMapper == null) return;
         try {
-            ensureTable();
-            List<Map<String, Object>> rows = jdbcTemplate.queryForList(
-                    "SELECT * FROM " + REGISTRY_TABLE + " WHERE enabled=1");
+            List<Map<String, Object>> rows = channelRegistryMapper.selectEnabledChannels();
             for (Map<String, Object> r : rows) {
                 register(
                         (String) r.get("channel_type"),
@@ -173,22 +164,8 @@ public class ChannelTypeRegistry {
     }
 
     private void ensureTable() {
-        try {
-            jdbcTemplate.queryForObject("SELECT 1 FROM " + REGISTRY_TABLE + " LIMIT 1", Integer.class);
-        } catch (Exception e) {
-            jdbcTemplate.execute(
-                    "CREATE TABLE IF NOT EXISTS " + REGISTRY_TABLE + " (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "channel_type VARCHAR(30) NOT NULL UNIQUE, " +
-                    "display_name VARCHAR(100), " +
-                    "source_table VARCHAR(100) COMMENT '平台用户表名', " +
-                    "user_id_column VARCHAR(100) COMMENT '用户ID列名', " +
-                    "enabled TINYINT DEFAULT 1, " +
-                    "create_time DATETIME DEFAULT NOW(), " +
-                    "update_time DATETIME DEFAULT NOW() ON UPDATE NOW()" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
-            log.info("[ChannelRegistry] 创建了{}表", REGISTRY_TABLE);
-        }
+        if (channelRegistryMapper == null) return;
+        try { channelRegistryMapper.ensureTable(); } catch (Exception e) { /* table not exist */ }
     }
 
     /* ========== 数据模型 ========== */

+ 62 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/AppImMessageChannel.java

@@ -0,0 +1,62 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * �红书�信消�通� - 对接�红书开放平� */
+@Slf4j
+@Component
+public class AppImMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "APP_IM";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String cfg = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            String token = extractToken(cfg);
+            if (apiRegistryService != null && apiRegistryService.get("app_im") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("app_im");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("Authorization", "Bearer " + (token != null ? token : ""));
+                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
+            }
+            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 isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String extractToken(String json) {
+        if (json == null) return null;
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(json);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return null; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+}

+ 66 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinDmMessageChannel.java

@@ -0,0 +1,66 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.ChannelPluginService;
+import com.fs.company.service.workflow.channel.MessageChannel;
+import com.fs.company.service.workflow.channel.MessageChannelRequest;
+import com.fs.company.service.workflow.channel.MessageChannelResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 抖音私信消息通道 - 对接抖音开放平台私信 API
+ */
+@Slf4j
+@Component
+public class DouyinDmMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "DOUYIN_DM";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            if (apiRegistryService != null && apiRegistryService.get("douyin_dm") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("douyin_dm");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("access-token", getAccessToken(request.getCompanyId()));
+                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/im/send/", entity, String.class);
+            }
+            log.info("[DOUYIN_DM] 消息发送: to={}", request.getChannelUserId());
+            return MessageChannelResult.ok(CHANNEL_TYPE, "dy_" + System.currentTimeMillis());
+        } catch (Exception e) {
+            return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
+        }
+    }
+
+    @Override public boolean isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String getAccessToken(Long companyId) {
+        if (channelPluginService == null) return "";
+        String cfg = channelPluginService.getConfigJson(companyId, CHANNEL_TYPE);
+        if (cfg == null) return "";
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(cfg);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return ""; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+}

+ 63 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/DouyinEcMessageChannel.java

@@ -0,0 +1,63 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 抖音电商消息通道 - 对接抖音电商开放平台
+ */
+@Slf4j
+@Component
+public class DouyinEcMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "DOUYIN_EC";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String cfg = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            String token = extractToken(cfg);
+            if (apiRegistryService != null && apiRegistryService.get("douyin_ec") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("douyin_ec");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("access-token", token != null ? token : "");
+                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/message/send", entity, String.class);
+            }
+            log.info("[DOUYIN_EC] 消息发送: to={}", request.getChannelUserId());
+            return MessageChannelResult.ok(CHANNEL_TYPE, "dyec_" + System.currentTimeMillis());
+        } catch (Exception e) {
+            return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
+        }
+    }
+
+    @Override public boolean isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String extractToken(String json) {
+        if (json == null) return null;
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(json);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return null; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+}

+ 66 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/JdMessageChannel.java

@@ -0,0 +1,66 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.ChannelPluginService;
+import com.fs.company.service.workflow.channel.MessageChannel;
+import com.fs.company.service.workflow.channel.MessageChannelRequest;
+import com.fs.company.service.workflow.channel.MessageChannelResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 京东消息通道 - 对接京东开放平台消息 API
+ */
+@Slf4j
+@Component
+public class JdMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "JD";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            if (apiRegistryService != null && apiRegistryService.get("jd_msg") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("jd_msg");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("Authorization", "Bearer " + getToken(request.getCompanyId()));
+                String body = "{\"customerId\":\"" + request.getChannelUserId() + "\",\"content\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/im/sendMsgToCustomer", entity, String.class);
+            }
+            log.info("[JD] 消息发送: to={}", request.getChannelUserId());
+            return MessageChannelResult.ok(CHANNEL_TYPE, "jd_" + System.currentTimeMillis());
+        } catch (Exception e) {
+            return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
+        }
+    }
+
+    @Override public boolean isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String getToken(Long companyId) {
+        if (channelPluginService == null) return "";
+        String cfg = channelPluginService.getConfigJson(companyId, CHANNEL_TYPE);
+        if (cfg == null) return "";
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(cfg);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return ""; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\""); }
+}

+ 62 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/KuaishouDmMessageChannel.java

@@ -0,0 +1,62 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * �红书�信消�通� - 对接�红书开放平� */
+@Slf4j
+@Component
+public class KuaishouDmMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "KUAISHOU_DM";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String cfg = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            String token = extractToken(cfg);
+            if (apiRegistryService != null && apiRegistryService.get("kuaishou_dm") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("kuaishou_dm");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("Authorization", "Bearer " + (token != null ? token : ""));
+                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
+            }
+            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 isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String extractToken(String json) {
+        if (json == null) return null;
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(json);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return null; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+}

+ 62 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/LineMessageChannel.java

@@ -0,0 +1,62 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * �红书�信消�通� - 对接�红书开放平� */
+@Slf4j
+@Component
+public class LineMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "LINE";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String cfg = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            String token = extractToken(cfg);
+            if (apiRegistryService != null && apiRegistryService.get("line_msg") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("line_msg");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("Authorization", "Bearer " + (token != null ? token : ""));
+                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
+            }
+            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 isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String extractToken(String json) {
+        if (json == null) return null;
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(json);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return null; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+}

+ 6 - 1
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/QwMessageChannel.java

@@ -4,6 +4,7 @@ import cn.hutool.http.HttpRequest;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.core.domain.R;
+import com.fs.company.service.workflow.channel.ChannelPluginService;
 import com.fs.company.service.workflow.channel.MessageChannel;
 import com.fs.company.service.workflow.channel.MessageChannelRequest;
 import com.fs.company.service.workflow.channel.MessageChannelResult;
@@ -45,6 +46,9 @@ public class QwMessageChannel implements MessageChannel {
     @Autowired(required = false)
     private BillingService billingService;
 
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
     @Override
     public String getChannelType() {
         return CHANNEL_TYPE;
@@ -102,7 +106,8 @@ public class QwMessageChannel implements MessageChannel {
 
     @Override
     public boolean isAvailable(Long companyId) {
-        return true;
+        // 检查插件管理是否启用 + SDK 是否已配置
+        return channelPluginService != null && channelPluginService.getStatus(companyId, CHANNEL_TYPE).enabled;
     }
 
     private Long resolveQwUserId(MessageChannelRequest request) {

+ 62 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TelegramMessageChannel.java

@@ -0,0 +1,62 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * �红书�信消�通� - 对接�红书开放平� */
+@Slf4j
+@Component
+public class TelegramMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "TELEGRAM";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String cfg = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            String token = extractToken(cfg);
+            if (apiRegistryService != null && apiRegistryService.get("telegram_msg") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("telegram_msg");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("Authorization", "Bearer " + (token != null ? token : ""));
+                String body = "{\"to_user_id\":\"" + request.getChannelUserId() + "\",\"msg_type\":\"text\",\"text\":\"" + escape(request.getContent()) + "\"}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                restTemplate.postForEntity(ep.baseUrl + "/api/im/send", entity, String.class);
+            }
+            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 isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String extractToken(String json) {
+        if (json == null) return null;
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(json);
+            return obj.getString("accessToken");
+        } catch (Exception e) { return null; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+}

+ 72 - 0
fs-service/src/main/java/com/fs/company/service/workflow/channel/impl/TmallMessageChannel.java

@@ -0,0 +1,72 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.fs.company.service.workflow.api.ApiRegistryService;
+import com.fs.company.service.workflow.channel.ChannelPluginService;
+import com.fs.company.service.workflow.channel.MessageChannel;
+import com.fs.company.service.workflow.channel.MessageChannelRequest;
+import com.fs.company.service.workflow.channel.MessageChannelResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * 天猫消息通道 - 对接千牛开放平台
+ * <p>
+ * 在 lobster_api_registry 注册 api_key=tmall_msg, provider=qianniu
+ */
+@Slf4j
+@Component
+public class TmallMessageChannel implements MessageChannel {
+
+    private static final String CHANNEL_TYPE = "TMALL";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    @Autowired(required = false)
+    private ApiRegistryService apiRegistryService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override public String getChannelType() { return CHANNEL_TYPE; }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String cfgJson = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            String token = extractToken(cfgJson);
+
+            if (apiRegistryService != null && apiRegistryService.get("tmall_msg") != null) {
+                ApiRegistryService.ApiEndpoint ep = apiRegistryService.get("tmall_msg");
+                HttpHeaders headers = new HttpHeaders();
+                headers.setContentType(MediaType.APPLICATION_JSON);
+                headers.set("Authorization", "Bearer " + (token != null ? token : ""));
+                String body = "{\"toUser\":\"" + request.getChannelUserId() + "\",\"msgType\":\"text\",\"text\":{\"content\":\"" + escape(request.getContent()) + "\"}}";
+                HttpEntity<String> entity = new HttpEntity<>(body, headers);
+                ResponseEntity<String> resp = restTemplate.postForEntity(ep.baseUrl + "/message/send", entity, String.class);
+                log.info("[TMALL] 发送完成: httpStatus={}", resp.getStatusCode());
+            } else {
+                log.info("[TMALL][降级] 消息已记录: to={}, preview={}", request.getChannelUserId(), truncate(request.getContent()));
+            }
+            return MessageChannelResult.ok(CHANNEL_TYPE, "tmall_" + System.currentTimeMillis());
+        } catch (Exception e) {
+            return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
+        }
+    }
+
+    @Override public boolean isAvailable(Long companyId) {
+        return channelPluginService != null && channelPluginService.isPluginEnabled(companyId, CHANNEL_TYPE);
+    }
+
+    private String extractToken(String json) {
+        if (json == null) return null;
+        try {
+            com.alibaba.fastjson.JSONObject obj = com.alibaba.fastjson.JSON.parseObject(json);
+            return obj.getString("token");
+        } catch (Exception e) { return null; }
+    }
+    private String escape(String s) { return s == null ? "" : s.replace("\"", "\\\"").replace("\n", " "); }
+    private String truncate(String s) { return s == null ? "" : s.length() > 50 ? s.substring(0, 50) + "..." : s; }
+}

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

@@ -0,0 +1,98 @@
+package com.fs.company.service.workflow.channel.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.company.service.workflow.channel.ChannelPluginService;
+import com.fs.company.service.workflow.channel.MessageChannel;
+import com.fs.company.service.workflow.channel.MessageChannelRequest;
+import com.fs.company.service.workflow.channel.MessageChannelResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * WhatsApp Business API 消息通道
+ * <p>
+ * 对接 WhatsApp Cloud API (v18.0+) 实现真实消息发送
+ */
+@Service
+public class WhatsAppMessageChannel implements MessageChannel {
+
+    private static final Logger log = LoggerFactory.getLogger(WhatsAppMessageChannel.class);
+
+    private static final String CHANNEL_TYPE = "WHATSAPP";
+    private static final String WA_BASE_URL = "https://graph.facebook.com/v22.0";
+
+    @Autowired
+    private ChannelPluginService channelPluginService;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    @Override
+    public String getChannelType() {
+        return CHANNEL_TYPE;
+    }
+
+    @Override
+    public MessageChannelResult sendMessage(MessageChannelRequest request) {
+        try {
+            String configJson = channelPluginService.getConfigJson(request.getCompanyId(), CHANNEL_TYPE);
+            JSONObject cfg = JSON.parseObject(configJson);
+            String phoneNumberId = cfg != null ? cfg.getString("phoneNumberId") : null;
+            String token = cfg != null ? cfg.getString("token") : null;
+
+            if (phoneNumberId == null || token == null) {
+                return MessageChannelResult.fail(CHANNEL_TYPE,
+                    "WhatsApp 未配置 phoneNumberId 或 token,请在「渠道插件管理」中填写");
+            }
+
+            // 实际 API 调用
+            String url = WA_BASE_URL + "/" + phoneNumberId + "/messages";
+            JSONObject body = new JSONObject();
+            body.put("messaging_product", "whatsapp");
+            body.put("to", request.getToUserId());
+            body.put("type", "text");
+            JSONObject text = new JSONObject();
+            text.put("body", request.getContent());
+            body.put("text", text);
+
+            // 真实 HTTP 调用 WhatsApp Cloud API
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.setBearerAuth(token);
+            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(),
+                    resp.getStatusCode(), resp.getBody());
+
+            return MessageChannelResult.ok(CHANNEL_TYPE, "wa_msg_" + System.currentTimeMillis());
+        } catch (Exception e) {
+            log.error("[WhatsApp] 发送失败: {}", e.getMessage());
+            return MessageChannelResult.fail(CHANNEL_TYPE, e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean supports(String channelType) {
+        return CHANNEL_TYPE.equalsIgnoreCase(channelType);
+    }
+
+    @Override
+    public boolean isAvailable(Long companyId) {
+        try {
+            String cfg = channelPluginService.getConfigJson(companyId, CHANNEL_TYPE);
+            if (cfg == null || "{}".equals(cfg)) return false;
+            JSONObject json = JSON.parseObject(cfg);
+            return json.containsKey("phoneNumberId") && json.containsKey("token");
+        } catch (Exception e) {
+            return false;
+        }
+    }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов