Ver Fonte

Merge remote-tracking branch 'origin/saas-api' into saas-api

yys há 3 dias atrás
pai
commit
cc8d6a4f25
100 ficheiros alterados com 5057 adições e 1333 exclusões
  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. 69 10
      fs-admin/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java
  5. 0 637
      fs-admin/src/main/java/com/fs/admin/controller/AdminLobsterBridgeController.java
  6. 245 0
      fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java
  7. 93 0
      fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceController.java
  8. 43 0
      fs-agent/src/main/resources/db/migration/tenant/V20260601_01__add_lobster_new_pages_menus.sql
  9. 1 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceApiController.java
  10. 9 0
      fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyWorkflowTagTemplateBindingController.java
  11. 7 4
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  12. 176 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAdminController.java
  13. 131 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterAiGeneratorController.java
  14. 10 23
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterBillingController.java
  15. 8 22
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEventAuditController.java
  16. 108 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterModelRouteController.java
  17. 30 8
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPromptController.java
  18. 9 12
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterSalesCorpusController.java
  19. 21 0
      fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java
  20. 71 0
      fs-company/src/main/java/com/fs/company/controller/workflow/PayCallbackController.java
  21. 52 0
      fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java
  22. 29 7
      fs-qw-api/src/main/java/com/fs/app/controller/QwExternalContactController.java
  23. 16 3
      fs-qw-api/src/main/java/com/fs/app/qwTask/qwTask.java
  24. 53 60
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApi.java
  25. 58 133
      fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApiTenant.java
  26. 6 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterNode.java
  27. 29 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChannelPluginConfig.java
  28. 22 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChatMsg.java
  29. 28 0
      fs-service/src/main/java/com/fs/company/domain/LobsterChatSession.java
  30. 20 0
      fs-service/src/main/java/com/fs/company/domain/LobsterComplianceAudit.java
  31. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterConsumeRecord.java
  32. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterDedupConfig.java
  33. 11 2
      fs-service/src/main/java/com/fs/company/domain/LobsterEventAudit.java
  34. 40 0
      fs-service/src/main/java/com/fs/company/domain/LobsterProfileConfig.java
  35. 36 0
      fs-service/src/main/java/com/fs/company/domain/LobsterSalesCorpus.java
  36. 42 0
      fs-service/src/main/java/com/fs/company/domain/LobsterSummaryConfig.java
  37. 40 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTenantBalance.java
  38. 33 0
      fs-service/src/main/java/com/fs/company/domain/LobsterTokenConsumption.java
  39. 4 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyLobsterTagUserRelMapper.java
  40. 1 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiMapper.java
  41. 32 58
      fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiTenantMapper.java
  42. 14 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java
  43. 29 0
      fs-service/src/main/java/com/fs/company/mapper/CustomerFactMapper.java
  44. 25 0
      fs-service/src/main/java/com/fs/company/mapper/CustomerHabitMapper.java
  45. 29 6
      fs-service/src/main/java/com/fs/company/mapper/LobsterApiRegistryMapper.java
  46. 203 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterAuxiliaryMapper.java
  47. 27 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterBillingMapper.java
  48. 44 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChannelPluginConfigMapper.java
  49. 35 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChannelRegistryMapper.java
  50. 18 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatMsgMapper.java
  51. 28 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatRecordMapper.java
  52. 36 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterChatSessionMapper.java
  53. 14 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceAuditMapper.java
  54. 2 4
      fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceRuleMapper.java
  55. 25 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterConversationSummaryMapper.java
  56. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterDedupConfigMapper.java
  57. 21 28
      fs-service/src/main/java/com/fs/company/mapper/LobsterDialogueStateMapper.java
  58. 17 12
      fs-service/src/main/java/com/fs/company/mapper/LobsterEventAuditMapper.java
  59. 88 5
      fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java
  60. 57 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterFeedbackMapper.java
  61. 11 5
      fs-service/src/main/java/com/fs/company/mapper/LobsterHandoffEventMapper.java
  62. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterKnowledgeUsageLogMapper.java
  63. 22 15
      fs-service/src/main/java/com/fs/company/mapper/LobsterLearningCorpusMapper.java
  64. 65 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterMultiTurnDialogueMapper.java
  65. 3 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterNodeExecutionLogMapper.java
  66. 49 27
      fs-service/src/main/java/com/fs/company/mapper/LobsterPendingKnowledgeMapper.java
  67. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterProfileConfigMapper.java
  68. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSalesCorpusMapper.java
  69. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSegmentMapper.java
  70. 17 7
      fs-service/src/main/java/com/fs/company/mapper/LobsterSensitiveWordMapper.java
  71. 13 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSmartApiMapper.java
  72. 22 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterSummaryConfigMapper.java
  73. 20 4
      fs-service/src/main/java/com/fs/company/mapper/LobsterSystemPromptMapper.java
  74. 93 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterTenantLearningMapper.java
  75. 33 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterTokenConsumptionMapper.java
  76. 37 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterToolCallMapper.java
  77. 23 13
      fs-service/src/main/java/com/fs/company/mapper/LobsterUserPreferenceMapper.java
  78. 31 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterUserProfileMapper.java
  79. 3 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterWorkflowInstanceMapper.java
  80. 40 0
      fs-service/src/main/java/com/fs/company/mapper/LobsterWorkflowVariableMapper.java
  81. 34 0
      fs-service/src/main/java/com/fs/company/mapper/ProfileEnrichmentMapper.java
  82. 17 0
      fs-service/src/main/java/com/fs/company/mapper/WhatsAppContactMapper.java
  83. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyTagTemplateBindingService.java
  84. 22 32
      fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiTenantService.java
  85. 96 8
      fs-service/src/main/java/com/fs/company/service/ai/AiSceneDispatcher.java
  86. 46 3
      fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java
  87. 6 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyTagTemplateBindingServiceImpl.java
  88. 39 4
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiServiceImpl.java
  89. 218 125
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiTenantServiceImpl.java
  90. 7 0
      fs-service/src/main/java/com/fs/company/service/llm/MultiModelRouter.java
  91. 155 37
      fs-service/src/main/java/com/fs/company/service/llm/impl/ModelRouterImpl.java
  92. 59 8
      fs-service/src/main/java/com/fs/company/service/llm/impl/MultiModelRouterImpl.java
  93. 103 0
      fs-service/src/main/java/com/fs/company/service/workflow/DynamicNodeImplService.java
  94. 32 0
      fs-service/src/main/java/com/fs/company/service/workflow/ILobsterBillingService.java
  95. 8 9
      fs-service/src/main/java/com/fs/company/service/workflow/ILobsterEventAuditService.java
  96. 18 0
      fs-service/src/main/java/com/fs/company/service/workflow/ILobsterSalesCorpusService.java
  97. 200 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterE2eTestService.java
  98. 9 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterEvolutionEngine.java
  99. 31 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterTestScenarioService.java
  100. 9 0
      fs-service/src/main/java/com/fs/company/service/workflow/LobsterWorkflowExecutor.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;

+ 69 - 10
fs-admin/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java

@@ -117,11 +117,11 @@ public class AdminCompanyBridgeController extends BaseController {
     }
 
     /** 查询租户已分配的接口列表(含定价信息) */
-    @GetMapping("/admin/voice-api/apis/{companyId}")
-    public AjaxResult voiceApiApis(@PathVariable Long companyId) {
+    @GetMapping("/admin/voice-api/apis/{tenantId}")
+    public AjaxResult voiceApiApis(@PathVariable Long tenantId) {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null ?
-            companyVoiceApiTenantService.selectEnabledApisByCompanyId(companyId) : new ArrayList<>();
+            companyVoiceApiTenantService.selectEnabledApisByTenantId(tenantId) : new ArrayList<>();
         return AjaxResult.success(list);
     }
 
@@ -133,22 +133,81 @@ public class AdminCompanyBridgeController extends BaseController {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
         Long apiId = Long.valueOf(body.get("apiId").toString());
-        @SuppressWarnings("unchecked")
-        List<Integer> companyIds = (List<Integer>) body.get("companyIds");
-        List<Long> ids = new ArrayList<>();
-        for (Integer id : companyIds) { ids.add(id.longValue()); }
-        companyVoiceApiTenantService.batchAssignTenants(apiId, ids);
+        List<Long> tenantIds = parseIdList(body.get("tenantIds"));
+        if (tenantIds.isEmpty()) {
+            tenantIds = parseIdList(body.get("companyIds"));
+        }
+        if (tenantIds.isEmpty()) {
+            return AjaxResult.error("请选择租户");
+        }
+        companyVoiceApiTenantService.batchAssignTenants(apiId, parseTenantAssignList(body, apiId, tenantIds));
         return AjaxResult.success();
     }
 
+    @SuppressWarnings("unchecked")
+    private List<CompanyVoiceApiTenant> parseTenantAssignList(Map<String, Object> body, Long apiId, List<Long> tenantIds) {
+        List<CompanyVoiceApiTenant> list = new ArrayList<>();
+        Object rawTenants = body.get("tenants");
+        if (rawTenants instanceof List) {
+            for (Object item : (List<?>) rawTenants) {
+                if (!(item instanceof Map)) {
+                    continue;
+                }
+                Map<String, Object> tenantMap = (Map<String, Object>) item;
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                if (tenantMap.get("tenantId") != null) {
+                    tenant.setTenantId(Long.valueOf(tenantMap.get("tenantId").toString()));
+                }
+                if (tenantMap.get("tenantCode") != null) {
+                    tenant.setTenantCode(tenantMap.get("tenantCode").toString());
+                }
+                if (tenantMap.get("tenantName") != null) {
+                    tenant.setTenantName(tenantMap.get("tenantName").toString());
+                }
+                if (tenant.getTenantId() != null) {
+                    list.add(tenant);
+                }
+            }
+        }
+        if (list.isEmpty()) {
+            for (Long tenantId : tenantIds) {
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                tenant.setTenantId(tenantId);
+                list.add(tenant);
+            }
+        }
+        return list;
+    }
+
     /** 取消分配 */
     @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
     @Log(title = "取消通话接口分配", businessType = BusinessType.DELETE)
     @DeleteMapping("/admin/voice-api/unassignTenant")
-    public AjaxResult unassignTenant(@RequestParam Long apiId, @RequestParam Long companyId) {
+    public AjaxResult unassignTenant(@RequestParam Long apiId,
+                                     @RequestParam(required = false) Long tenantId,
+                                     @RequestParam(required = false) Long companyId) {
         DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
         if (companyVoiceApiTenantService == null) return AjaxResult.error("服务未就绪");
-        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, companyId));
+        Long tid = tenantId != null ? tenantId : companyId;
+        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, tid));
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<Long> parseIdList(Object raw) {
+        List<Long> ids = new ArrayList<>();
+        if (raw == null) {
+            return ids;
+        }
+        if (raw instanceof List) {
+            for (Object item : (List<?>) raw) {
+                if (item != null) {
+                    ids.add(Long.valueOf(item.toString()));
+                }
+            }
+        }
+        return ids;
     }
 
     /** 查询接口已分配租户数量 */

+ 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<>());
-    }
-}

+ 245 - 0
fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceApiTenantController.java

@@ -0,0 +1,245 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+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.BusinessType;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.domain.CompanyVoiceApiTenant;
+import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.company.service.ICompanyVoiceApiTenantService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 外呼接口-租户分配与定价管理(新版 admin 专用)
+ *
+ * @author MixLiu
+ */
+@RestController
+@RequestMapping("/admin/companyVoiceApiTenant")
+public class CompanyVoiceApiTenantController extends BaseController {
+
+    @Autowired(required = false)
+    private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
+
+    @Autowired(required = false)
+    private ICompanyVoiceApiService companyVoiceApiService;
+
+    /**
+     * 分页查询租户-接口绑定/定价列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceApiTenant param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService != null
+                ? companyVoiceApiTenantService.selectCompanyVoiceApiTenantList(param)
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    /**
+     * 外呼接口下拉选项(新增绑定时选择接口)
+     */
+    @GetMapping("/apiList")
+    public TableDataInfo apiList() {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        List<CompanyVoiceApi> list = companyVoiceApiService != null
+                ? companyVoiceApiService.selectCompanyVoiceApiList(new CompanyVoiceApi())
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    /**
+     * 分配接口给租户(批量)
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "分配通话接口给租户", businessType = BusinessType.INSERT)
+    @PostMapping("/assign")
+    public AjaxResult assign(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        Long apiId = Long.valueOf(body.get("apiId").toString());
+        List<Long> tenantIds = parseIdList(body.get("tenantIds"));
+        if (tenantIds.isEmpty()) {
+            tenantIds = parseIdList(body.get("companyIds"));
+        }
+        if (tenantIds.isEmpty()) {
+            return AjaxResult.error("请选择租户");
+        }
+        companyVoiceApiTenantService.batchAssignTenants(apiId, parseTenantAssignList(body, apiId, tenantIds));
+        return AjaxResult.success();
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<CompanyVoiceApiTenant> parseTenantAssignList(Map<String, Object> body, Long apiId, List<Long> tenantIds) {
+        List<CompanyVoiceApiTenant> list = new ArrayList<>();
+        Object rawTenants = body.get("tenants");
+        if (rawTenants instanceof List) {
+            for (Object item : (List<?>) rawTenants) {
+                if (!(item instanceof Map)) {
+                    continue;
+                }
+                Map<String, Object> tenantMap = (Map<String, Object>) item;
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                if (tenantMap.get("tenantId") != null) {
+                    tenant.setTenantId(Long.valueOf(tenantMap.get("tenantId").toString()));
+                }
+                if (tenantMap.get("tenantCode") != null) {
+                    tenant.setTenantCode(tenantMap.get("tenantCode").toString());
+                }
+                if (tenantMap.get("tenantName") != null) {
+                    tenant.setTenantName(tenantMap.get("tenantName").toString());
+                }
+                if (tenant.getTenantId() != null) {
+                    list.add(tenant);
+                }
+            }
+        }
+        if (list.isEmpty()) {
+            for (Long tenantId : tenantIds) {
+                CompanyVoiceApiTenant tenant = new CompanyVoiceApiTenant();
+                tenant.setApiId(apiId);
+                tenant.setTenantId(tenantId);
+                list.add(tenant);
+            }
+        }
+        return list;
+    }
+
+    /**
+     * 取消分配
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "取消通话接口分配", businessType = BusinessType.DELETE)
+    @DeleteMapping("/unassign")
+    public AjaxResult unassign(@RequestParam Long apiId,
+                               @RequestParam(required = false) Long tenantId,
+                               @RequestParam(required = false) Long companyId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        Long tid = tenantId != null ? tenantId : companyId;
+        return toAjax(companyVoiceApiTenantService.unassignTenant(apiId, tid));
+    }
+
+    /**
+     * 更新租户定价/绑定配置
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "更新外呼租户定价", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult update(@RequestBody CompanyVoiceApiTenant data) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        if (data.getId() == null && data.getApiId() != null && data.getTenantId() != null) {
+            CompanyVoiceApiTenant existing = companyVoiceApiTenantService.selectByApiAndTenant(
+                    data.getApiId(), data.getTenantId());
+            if (existing != null) {
+                data.setId(existing.getId());
+            }
+        }
+        return toAjax(companyVoiceApiTenantService.updateCompanyVoiceApiTenant(data));
+    }
+
+    /**
+     * 批量更新租户定价/绑定配置
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "批量更新外呼租户定价", businessType = BusinessType.UPDATE)
+    @PutMapping("/batchPricing")
+    public AjaxResult batchPricing(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        List<Long> ids = parseIdList(body.get("ids"));
+        if (ids.isEmpty()) {
+            return AjaxResult.error("请选择要更新的记录");
+        }
+        CompanyVoiceApiTenant pricing = parsePricingFields(body);
+        if (pricing.getSalePrice() == null && pricing.getPriority() == null
+                && pricing.getIsPrimary() == null && pricing.getSelectable() == null) {
+            return AjaxResult.error("请至少填写一项要批量更新的配置");
+        }
+        return toAjax(companyVoiceApiTenantService.batchUpdatePricing(ids, pricing));
+    }
+
+    /**
+     * 批量更新状态
+     */
+    @PreAuthorize("@ss.hasPermi('company:companyVoiceApi:edit')")
+    @Log(title = "批量更新外呼租户状态", businessType = BusinessType.UPDATE)
+    @PutMapping("/batchStatus")
+    public AjaxResult batchStatus(@RequestBody Map<String, Object> body) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiTenantService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        List<Long> ids = parseIdList(body.get("ids"));
+        if (ids.isEmpty()) {
+            return AjaxResult.error("请选择要更新的记录");
+        }
+        if (body.get("status") == null) {
+            return AjaxResult.error("请指定状态");
+        }
+        Integer status = Integer.valueOf(body.get("status").toString());
+        if (status != 0 && status != 1) {
+            return AjaxResult.error("状态值无效");
+        }
+        return toAjax(companyVoiceApiTenantService.batchUpdateStatus(ids, status));
+    }
+
+    private CompanyVoiceApiTenant parsePricingFields(Map<String, Object> body) {
+        CompanyVoiceApiTenant pricing = new CompanyVoiceApiTenant();
+        if (body.get("salePrice") != null && StringUtils.isNotEmpty(body.get("salePrice").toString())) {
+            pricing.setSalePrice(new java.math.BigDecimal(body.get("salePrice").toString()));
+        }
+        if (body.get("priority") != null && StringUtils.isNotEmpty(body.get("priority").toString())) {
+            pricing.setPriority(Integer.valueOf(body.get("priority").toString()));
+        }
+        if (body.get("isPrimary") != null && StringUtils.isNotEmpty(body.get("isPrimary").toString())) {
+            pricing.setIsPrimary(Integer.valueOf(body.get("isPrimary").toString()));
+        }
+        Object selectable = body.get("selectable");
+        if (selectable == null) {
+            selectable = body.get("allowManual");
+        }
+        if (selectable != null && StringUtils.isNotEmpty(selectable.toString())) {
+            pricing.setSelectable(selectable.toString());
+        }
+        return pricing;
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<Long> parseIdList(Object raw) {
+        List<Long> ids = new ArrayList<>();
+        if (raw == null) {
+            return ids;
+        }
+        if (raw instanceof List) {
+            for (Object item : (List<?>) raw) {
+                if (item != null) {
+                    ids.add(Long.valueOf(item.toString()));
+                }
+            }
+        }
+        return ids;
+    }
+}

+ 93 - 0
fs-admin/src/main/java/com/fs/admin/controller/CompanyVoiceController.java

@@ -0,0 +1,93 @@
+package com.fs.admin.controller;
+
+import com.fs.common.annotation.Log;
+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.BusinessType;
+import com.fs.common.enums.DataSourceType;
+import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 外呼接口管理(新版 admin 专用)
+ *
+ * @author MixLiu
+ */
+@RestController
+@RequestMapping("/admin/companyVoice")
+public class CompanyVoiceController extends BaseController {
+
+    @Autowired(required = false)
+    private ICompanyVoiceApiService companyVoiceApiService;
+
+    /**
+     * 分页查询外呼接口列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(CompanyVoiceApi param) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        startPage();
+        List<CompanyVoiceApi> list = companyVoiceApiService != null
+                ? companyVoiceApiService.selectCompanyVoiceApiList(param)
+                : java.util.Collections.emptyList();
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取外呼接口详情
+     */
+    @GetMapping("/{apiId}")
+    public AjaxResult getInfo(@PathVariable Long apiId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        CompanyVoiceApi api = companyVoiceApiService.selectCompanyVoiceApiById(apiId);
+        return AjaxResult.success(api);
+    }
+
+    /**
+     * 新增外呼接口
+     */
+    @Log(title = "外呼接口", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody CompanyVoiceApi companyVoiceApi) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        return toAjax(companyVoiceApiService.insertCompanyVoiceApi(companyVoiceApi));
+    }
+
+    /**
+     * 修改外呼接口
+     */
+    @Log(title = "外呼接口", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody CompanyVoiceApi companyVoiceApi) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        return toAjax(companyVoiceApiService.updateCompanyVoiceApi(companyVoiceApi));
+    }
+
+    /**
+     * 删除外呼接口(逻辑删除)
+     */
+    @Log(title = "外呼接口", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{apiId}")
+    public AjaxResult remove(@PathVariable Long apiId) {
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (companyVoiceApiService == null) {
+            return AjaxResult.error("服务未就绪");
+        }
+        return toAjax(companyVoiceApiService.deleteCompanyVoiceApiById(apiId));
+    }
+}

+ 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;

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyVoiceApiController.java

@@ -87,7 +87,7 @@ public class CompanyVoiceApiController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany() != null ? loginUser.getCompany().getCompanyId() : null;
         if (companyId == null) { return AjaxResult.error("请选择租户"); }
-        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService.selectEnabledApisByCompanyId(companyId);
+        List<CompanyVoiceApiTenant> list = companyVoiceApiTenantService.selectEnabledApisByTenantId(companyId);
         return AjaxResult.success(list);
     }
 

+ 9 - 0
fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyWorkflowTagTemplateBindingController.java

@@ -125,4 +125,13 @@ public class CompanyWorkflowTagTemplateBindingController extends BaseController
                 loginUser.getCompany().getCompanyId(), loginUser.getUsername(),
                 param.getQwCorpId(), param.getUserIds(), param.getTagCodes(),loginUser.getUser().getUserId());
     }
+
+    /**
+     * 企微客户获取龙虾标签
+     */
+    @PostMapping("/lobsterTags")
+    public AjaxResult lobsterTags(@RequestBody List<Long> userIds) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return tagTemplateBindingService.lobsterTags(userIds,loginUser.getUser().getUserId(),loginUser.getCompany().getCompanyId());
+    }
 }

+ 7 - 4
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -56,6 +56,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
 import java.io.IOException;
 import java.net.SocketTimeoutException;
@@ -203,8 +204,8 @@ public class QwExternalContactController extends BaseController
     @Log(title = "同步企业微信客户", businessType = BusinessType.INSERT)
     @PostMapping
     public R add(@RequestBody QwExternalContact qwExternalContact) throws IOException {
-
-        return qwExternalContactService.syncQwExternalContact(qwExternalContact.getCorpId());
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return qwExternalContactService.syncQwExternalContact(qwExternalContact.getCorpId(),loginUser.getTenantId());
     }
 
     /**
@@ -331,8 +332,10 @@ public class QwExternalContactController extends BaseController
 
     @Log(title = "同步我的企业微信客户", businessType = BusinessType.INSERT)
     @GetMapping("/syncMyExternalContact/{id}")
-    public R  syncMyExternalContact(@PathVariable("id") Long id ) throws IOException {
-        return qwExternalContactService.syncMyQwExternalContact(id);
+    public R  syncMyExternalContact(@PathVariable("id") Long id, HttpServletRequest request ) throws IOException {
+        LoginUser loginUser = tokenService.getLoginUser(request);
+        Long tenantId = loginUser.getTenantId();
+        return qwExternalContactService.syncMyQwExternalContact(id,tenantId);
     }
 
 

+ 176 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/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");
+    }
+}

+ 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 - 23
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterBillingController.java

@@ -3,7 +3,8 @@ 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;
@@ -22,8 +23,8 @@ import java.util.*;
 @RequestMapping("/workflow/lobster/billing")
 public class LobsterBillingController extends BaseController {
 
-    @Autowired(required = false)
-    private BillingService billingService;
+    @Autowired
+    private ILobsterBillingService billingService;
 
     @Autowired
     private TokenService tokenService;
@@ -35,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);
@@ -66,12 +68,8 @@ public class LobsterBillingController extends BaseController {
             return AjaxResult.error("系数范围: 0.01 ~ 100");
         }
 
-        if (billingService != null) {
-            boolean ok = billingService.updateTokenCoefficient(tenantId, coefficient);
-            return ok ? AjaxResult.success("Token系数已更新为 " + coefficient) : AjaxResult.error("更新失败");
-        }
-
-        return AjaxResult.error("计费服务未初始化");
+        boolean ok = billingService.updateTokenCoefficient(tenantId, coefficient);
+        return ok ? AjaxResult.success("Token系数已更新为 " + coefficient) : AjaxResult.error("更新失败");
     }
 
     /** 消费记录列表 */
@@ -82,18 +80,7 @@ public class LobsterBillingController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long tenantId = loginUser.getCompany().getCompanyId();
 
-        Map<String, Object> result = new LinkedHashMap<>();
-        if (billingService != null) {
-            List<Map<String, Object>> list = billingService.getConsumeRecords(tenantId, page, size);
-            long total = billingService.countConsumeRecords(tenantId);
-            result.put("list", list);
-            result.put("total", total);
-        } else {
-            result.put("list", Collections.emptyList());
-            result.put("total", 0);
-        }
-        result.put("page", page);
-        result.put("size", size);
+        Map<String, Object> result = billingService.listConsumeRecords(page, size, tenantId);
         return AjaxResult.success(result);
     }
 

+ 8 - 22
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterEventAuditController.java

@@ -28,7 +28,7 @@ import java.util.*;
 public class LobsterEventAuditController extends BaseController {
 
     @Autowired
-    private ILobsterEventAuditService eventAuditService;
+    private ILobsterEventAuditService auditService;
 
     @Autowired
     private TokenService tokenService;
@@ -47,20 +47,7 @@ public class LobsterEventAuditController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
 
-        List<LobsterEventAudit> list = eventAuditService.selectListByCompanyIdAndStatus(companyId, status, page, size);
-        long total = eventAuditService.countByCompanyIdAndStatus(companyId, status);
-
-        Map<String, Object> stats = new LinkedHashMap<>();
-        stats.put("pending", eventAuditService.countByCompanyIdAndStatus(companyId, "pending"));
-        stats.put("approved", eventAuditService.countByCompanyIdAndStatus(companyId, "approved"));
-        stats.put("rejected", eventAuditService.countByCompanyIdAndStatus(companyId, "rejected"));
-
-        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);
     }
 
@@ -72,8 +59,8 @@ public class LobsterEventAuditController extends BaseController {
     public AjaxResult approve(@PathVariable Long id) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
-        LobsterEventAudit record = eventAuditService.selectByIdAndStatus(id, "pending");
-        if (record == null) {
+        LobsterEventAudit record = auditService.getById(id);
+        if (record == null || !"pending".equals(record.getStatus())) {
             return AjaxResult.error("审核记录不存在或已处理");
         }
 
@@ -91,8 +78,7 @@ public class LobsterEventAuditController extends BaseController {
             }
         }
 
-        String comment = patched ? "审核通过,节点已注入" : "审核通过";
-        eventAuditService.approve(id, loginUser.getUsername(), comment);
+        auditService.approve(id, loginUser.getUsername());
 
         Map<String, Object> result = new HashMap<>();
         result.put("patched", patched);
@@ -109,9 +95,9 @@ public class LobsterEventAuditController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
         String comment = body != null ? (String) body.getOrDefault("comment", "人工驳回") : "人工驳回";
-        eventAuditService.reject(id, loginUser.getUsername(), comment);
+        boolean ok = auditService.reject(id, loginUser.getUsername(), comment);
 
-        return AjaxResult.success("已驳回");
+        return ok ? AjaxResult.success("已驳回") : AjaxResult.error("审核记录不存在");
     }
 
     /**
@@ -120,7 +106,7 @@ public class LobsterEventAuditController extends BaseController {
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/{id}")
     public AjaxResult detail(@PathVariable Long id) {
-        LobsterEventAudit record = eventAuditService.selectById(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;
+    }
+}

+ 30 - 8
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterPromptController.java

@@ -11,7 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Map;
+import java.util.*;
 
 /**
  * 龙虾系统提示词管理Controller
@@ -41,23 +41,44 @@ public class LobsterPromptController extends BaseController {
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/{id}")
     public AjaxResult getById(@PathVariable Long id) {
-        LobsterSystemPrompt prompt = promptService.getById(id);
-        return AjaxResult.success(prompt);
+        LobsterSystemPrompt row = promptService.getById(id);
+        return AjaxResult.success(row);
     }
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PostMapping
-    public AjaxResult create(@RequestBody LobsterSystemPrompt body) {
+    public AjaxResult create(@RequestBody Map<String, Object> body) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        promptService.create(body, loginUser.getUsername(), loginUser.getCompany().getCompanyId());
+        String content = (String) body.get("promptContent");
+        if (content == null || content.isEmpty()) return AjaxResult.error("提示词内容不能为空");
+
+        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("创建成功");
     }
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")
     @PutMapping("/{id}")
-    public AjaxResult update(@PathVariable Long id, @RequestBody LobsterSystemPrompt body) {
+    public AjaxResult update(@PathVariable Long id, @RequestBody Map<String, Object> body) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        promptService.update(id, body, loginUser.getCompany().getCompanyId());
+
+        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("更新成功");
     }
 
@@ -70,7 +91,8 @@ public class LobsterPromptController extends BaseController {
 
     @GetMapping("/categories")
     public AjaxResult categories() {
-        return AjaxResult.success(promptService.getCategories());
+        List<String> cats = promptService.getCategories();
+        return AjaxResult.success(cats);
     }
 
     @PreAuthorize("@ss.hasPermi('workflow:lobster:edit')")

+ 9 - 12
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterSalesCorpusController.java

@@ -3,10 +3,8 @@ package com.fs.company.controller.workflow;
 import com.alibaba.fastjson.JSONObject;
 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.utils.ServletUtils;
-import com.fs.company.domain.LobsterLearningCorpus;
-import com.fs.company.service.workflow.ILobsterLearningCorpusService;
+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;
@@ -21,7 +19,7 @@ import java.util.*;
 /**
  * 龙虾销冠语料管理Controller
  *
- * 表: lobster_learning_corpus
+ * 表: lobster_sales_corpus
  * 页面: 销冠语料 → 录入/批量导入/AI分析/话术库查询
  *
  * 核心价值: 租户上传销冠/金牌客服聊天话术 → AI分析提取沟通模式 → 进化引擎学习 → 全租户共享
@@ -34,7 +32,7 @@ public class LobsterSalesCorpusController extends BaseController {
     private SalesCorpusAnalyzer corpusAnalyzer;
 
     @Autowired
-    private ILobsterLearningCorpusService corpusService;
+    private ILobsterSalesCorpusService salesCorpusService;
 
     @Autowired
     private TokenService tokenService;
@@ -150,16 +148,15 @@ public class LobsterSalesCorpusController extends BaseController {
      */
     @PreAuthorize("@ss.hasPermi('workflow:lobster:query')")
     @GetMapping("/list")
-    public TableDataInfo list(@RequestParam(defaultValue = "1") int page,
-                              @RequestParam(defaultValue = "10") int size,
-                              @RequestParam(required = false) String scenario,
-                              @RequestParam(required = false) String status) {
+    public AjaxResult list(@RequestParam(defaultValue = "1") int page,
+                           @RequestParam(defaultValue = "10") int size,
+                           @RequestParam(required = false) String scenario,
+                           @RequestParam(required = false) String status) {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         Long companyId = loginUser.getCompany().getCompanyId();
 
-        startPage();
-        List<LobsterLearningCorpus> list = corpusService.selectListByCompanyId(companyId, scenario, status);
-        return getDataTable(list);
+        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);
+    }
 }

+ 71 - 0
fs-company/src/main/java/com/fs/company/controller/workflow/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) : "";
+    }
+}

+ 52 - 0
fs-qw-api/src/main/java/com/fs/app/controller/OpenQwApiController.java

@@ -9,6 +9,9 @@ import com.fs.framework.datasource.TenantDataSourceUtil;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwExternalContactAddTagParam;
 import com.fs.qw.param.QwExternalContactUpdateNoteParam;
+import com.fs.qwApi.domain.QwExternalContactAllListResult;
+import com.fs.qwApi.domain.QwExternalContactListResult;
+import com.fs.qwApi.param.QwAllExternalcontactListParam;
 import com.fs.qwApi.param.QwUploadImageByCourseParam;
 import com.fs.qwApi.service.QwApiService;
 import lombok.extern.slf4j.Slf4j;
@@ -214,5 +217,54 @@ public class OpenQwApiController extends BaseController {
             return R.error("上传图片失败: " + e.getMessage());
         }
     }
+
+    /**
+     * 按企微用户查询外部联系人
+     */
+    @PostMapping("/getExternalcontactList")
+    public QwExternalContactListResult getExternalcontactList(@RequestParam("qwUserId") String qwUserId,
+                                                              @RequestParam("corpId") String corpId,
+                                                              @RequestParam("tenantId") Long tenantId) {
+        log.info("按企微用户查询外部联系人,tenantId={}, qwUserId={}, corpId={}", tenantId, qwUserId, corpId);
+
+        try {
+            return tenantDataSourceUtil.executeWithResult(tenantId, () ->
+                    qwApiService.getExternalcontactList(qwUserId, corpId)
+            );
+        } catch (Exception e) {
+            log.error("[QwFriendWelcome] 按企微用户查询外部联系人失败,tenantId={}, qwUserId={}, corpId={}",
+                    tenantId, qwUserId, corpId, e);
+
+            QwExternalContactListResult result = new QwExternalContactListResult();
+            result.setErrcode(1);
+            result.setErrmsg("按企微用户查询外部联系人失败: " + e.getMessage());
+            return result;
+        }
+    }
+
+    /**
+     * 批量获取客户详情
+     */
+    @PostMapping("/getAllExternalcontactList")
+    public QwExternalContactAllListResult getAllExternalcontactList(@RequestBody QwAllExternalcontactListParam param) {
+        log.info("批量获取客户详情,参数{}", param);
+        Long tenantId = param.getTenantId();
+        if (tenantId == null){
+            return null;
+        }
+        try {
+            return tenantDataSourceUtil.executeWithResult(tenantId, () ->
+                    qwApiService.getAllExternalcontactList(param, param.getCorpId(), param.getQwCompany())
+            );
+        } catch (Exception e) {
+            log.error("批量获取客户详情失败,tenantId={}, QwCompany={}, corpId={}",
+                    tenantId, param.getQwCompany(), param.getCorpId(), e);
+            QwExternalContactAllListResult result = new QwExternalContactAllListResult();
+            result.setErrcode(1);
+            result.setErrmsg("按企微用户查询外部联系人失败: " + e.getMessage());
+            return result;
+        }
+    }
+
 }
 

+ 29 - 7
fs-qw-api/src/main/java/com/fs/app/controller/QwExternalContactController.java

@@ -1,19 +1,24 @@
 package com.fs.app.controller;
 
+import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.bean.BeanUtils;
 import com.fs.framework.datasource.TenantDataSourceUtil;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qwApi.config.OpenQwConfig;
 import com.fs.qwApi.domain.QwExternalContactAllListResult;
 import com.fs.qwApi.domain.QwUnassignedListResult;
 import com.fs.qwApi.domain.inner.ExternalContact;
 import com.fs.qwApi.domain.inner.ExternalContactInfo;
 import com.fs.qwApi.domain.inner.FollowInfo;
+import com.fs.qwApi.param.QwAllExternalcontactListParam;
 import com.fs.qwApi.param.QwExternalListParam;
 import com.fs.qwApi.param.QwUnassignedListParam;
 import com.fs.qwApi.service.QwApiService;
@@ -51,7 +56,7 @@ public class QwExternalContactController extends BaseController {
     /**
      * 同步企微外部联系人(递归分页)
      *
-     * @param qwUser      企微用户信息(JSON)
+     * @param qwUserJson      企微用户信息(JSON)
      * @param cursor      分页游标
      * @param tenantId    租户ID
      */
@@ -63,14 +68,14 @@ public class QwExternalContactController extends BaseController {
         return tenantDataSourceUtil.executeWithResult(tenantId, () -> {
             // 从JSON还原QwUser对象
             QwUser qwUser = JSON.parseObject(qwUserJson, QwUser.class);
-            return doSyncExternalContact(qwUser, cursor);
+            return doSyncExternalContact(qwUser, cursor,tenantId);
         });
     }
 
     /**
      * 递归同步外部联系人
      */
-    private R doSyncExternalContact(QwUser qwUser, String getNextCursor) {
+    private R doSyncExternalContact(QwUser qwUser, String getNextCursor,Long tenantId) {
         String qwUserId = qwUser.getQwUserId();
         String corpId = qwUser.getCorpId();
         Long companyId = qwUser.getCompanyId();
@@ -80,9 +85,26 @@ public class QwExternalContactController extends BaseController {
         param.setUserid_list(Arrays.asList(qwUserId));
         param.setCursor(getNextCursor);
         QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
-        QwExternalContactAllListResult list = qwApiService.getAllExternalcontactList(param, corpId, qwCompany);
-
-        log.info("批量获取客户详情 {}", list);
+        QwExternalContactAllListResult list = new QwExternalContactAllListResult();
+//        QwExternalContactAllListResult list = qwApiService.getAllExternalcontactList(param, corpId, qwCompany);
+        QwAllExternalcontactListParam qwAllExternalcontactListParam = new QwAllExternalcontactListParam();
+        BeanUtils.copyProperties(param, qwAllExternalcontactListParam);
+        qwAllExternalcontactListParam.setCorpId(corpId);
+        qwAllExternalcontactListParam.setQwCompany(qwCompany);
+        qwAllExternalcontactListParam.setTenantId(tenantId);
+        String url = OpenQwConfig.baseApi + "/getAllExternalcontactList";
+        try {
+            String result = HttpUtil.createPost(url)
+                    .body(JSON.toJSONString(qwAllExternalcontactListParam))
+                    .execute()
+                    .body();
+            log.info("批量获取客户详情: result={}", result);
+            if (StringUtils.isNotBlank(result)){
+                list = JSON.parseObject(result, QwExternalContactAllListResult.class);
+            }
+        } catch (Exception e) {
+            log.error("批量获取客户详情失败: param:{}", qwAllExternalcontactListParam);
+        }
 
         if (list.getErrcode() == 0) {
             List<ExternalContactInfo> externalContactList = list.getExternal_contact_list();
@@ -137,7 +159,7 @@ public class QwExternalContactController extends BaseController {
         }
 
         if (!StringUtil.strIsNullOrEmpty(list.getNext_cursor())) {
-            return doSyncExternalContact(qwUser, list.getNext_cursor());
+            return doSyncExternalContact(qwUser, list.getNext_cursor(),tenantId);
         }
         return R.ok();
     }

+ 16 - 3
fs-qw-api/src/main/java/com/fs/app/qwTask/qwTask.java

@@ -1,6 +1,7 @@
 package com.fs.app.qwTask;
 
 import com.fs.app.service.OpenQwApiService;
+import com.fs.common.config.RedisTenantContext;
 import com.fs.course.service.IFinishCourseStatisticsSyncService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.qw.domain.QwIpadServerLog;
@@ -13,6 +14,7 @@ import com.fs.sop.service.impl.QwSopServiceImpl;
 import com.fs.sop.service.ISopUserLogsService;
 import com.fs.statis.IFsStatisQwWatchService;
 import com.fs.statis.service.FsStatisSalerWatchService;
+import com.fs.tenant.domain.TenantInfo;
 import com.fs.wxwork.dto.WxWorkGetQrCodeDTO;
 import com.fs.wxwork.service.WxWorkService;
 import lombok.extern.slf4j.Slf4j;
@@ -29,6 +31,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.List;
 import java.util.Optional;
+import java.util.function.Consumer;
 
 @Component
 @Slf4j
@@ -105,18 +108,28 @@ public class qwTask {
      * 否则直接执行(兼容单库模式或被 TenantTaskRunner 调用时)。
      */
     private void runWithSaaSTenant(String taskName, Runnable action) {
+        runWithSaaSTenant(taskName, tenant -> action.run());
+    }
+
+    /**
+     * SaaS 多租户定时任务:按租户切库后执行,并将当前租户信息传给业务逻辑。
+     */
+    private void runWithSaaSTenant(String taskName, Consumer<TenantInfo> tenantAction) {
         if (saasTaskEnabled && !TenantTaskRunner.isInTenantExecution()) {
-            tenantTaskRunner.runForEachTenant(taskName, action);
+            tenantTaskRunner.runForEachTenant(taskName, tenantAction);
             return;
         }
-        action.run();
+        TenantInfo tenant = new TenantInfo();
+        tenant.setId(RedisTenantContext.getTenantId());
+        tenantAction.accept(tenant);
     }
 
     //正在使用
     @Scheduled(cron = "0 0 1 * * ?")
     public void qwExternalContact()
     {
-        runWithSaaSTenant("qwExternalContact", () -> qwExternalContactService.qwExternalContactSync());
+        runWithSaaSTenant("qwExternalContact", tenant ->
+                qwExternalContactService.qwExternalContactSync(tenant.getId()));
     }
     //正在使用
 //    @Scheduled(cron = "1/1 * * * * ?")

+ 53 - 60
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApi.java

@@ -2,79 +2,72 @@ package com.fs.company.domain;
 
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
+import java.math.BigDecimal;
+
 /**
  * 呼叫接口对象 company_voice_api
- * 
+ *
  * @author fs
  * @date 2021-10-04
  */
-public class CompanyVoiceApi extends BaseEntity
-{
+@Data
+public class CompanyVoiceApi extends BaseEntity {
     private static final long serialVersionUID = 1L;
 
-    /** ID */
     private Long apiId;
-
-    /** API接口名 */
-    @Excel(name = "API接口名")
+    /**
+     * API名称
+     */
     private String apiName;
-
-    /** KEY */
-    @Excel(name = "apiType")
-    private String apiType;
-
-    /** JSON */
-    @Excel(name = "JSON")
-    private String apiJson;
-
-    /** 状态 */
-    @Excel(name = "状态")
+    /**
+     * 接口类型:0 SIP,1 网关,2 API
+     */
+    private Integer apiType;
+
+    /**
+     * 状态: 0 禁用 ,1 启用
+     */
     private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 成本价
+     */
+    private BigDecimal costPrice;
+    /**
+     * 账户
+     */
+    private String account;
+    /**
+     * 密码
+     */
+    private String password;
+    /**
+     * api地址
+     */
+    private String apiUrl;
+    /**
+     * 话术跳转地址(API类型)
+     */
+    private String dialogUrl;
+    /**
+     * 服务商
+     */
+    private String provider;
+    /**
+     * 历史字段,保留列暂不读写
+     */
+    private String apiJson;
 
-    public static long getSerialVersionUID() {
-        return serialVersionUID;
-    }
-
-    public Long getApiId() {
-        return apiId;
-    }
-
-    public void setApiId(Long apiId) {
-        this.apiId = apiId;
-    }
-
-    public String getApiName() {
-        return apiName;
-    }
-
-    public void setApiName(String apiName) {
-        this.apiName = apiName;
-    }
-
-    public String getApiType() {
-        return apiType;
-    }
-
-    public void setApiType(String apiType) {
-        this.apiType = apiType;
-    }
-
-    public String getApiJson() {
-        return apiJson;
-    }
-
-    public void setApiJson(String apiJson) {
-        this.apiJson = apiJson;
-    }
-
-    public Integer getStatus() {
-        return status;
-    }
+    /**
+     * 是否删除,0否 1是
+     */
+    private Integer isDel;
 
-    public void setStatus(Integer status) {
-        this.status = status;
-    }
 }

+ 58 - 133
fs-service/src/main/java/com/fs/company/domain/CompanyVoiceApiTenant.java

@@ -1,7 +1,9 @@
 package com.fs.company.domain;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
 
 import java.math.BigDecimal;
 import java.util.Date;
@@ -12,164 +14,87 @@ import java.util.Date;
  * @author fs
  * @date 2026-05-21
  */
-public class CompanyVoiceApiTenant extends BaseEntity
-{
+@Data
+public class CompanyVoiceApiTenant extends BaseEntity {
     private static final long serialVersionUID = 1L;
 
-    /** 主键 */
+    /**
+     * 主键
+     */
     private Long id;
 
-    /** 通话接口ID */
+    /**
+     * 通话接口ID
+     */
     private Long apiId;
 
-    /** 租户ID */
-    private Long companyId;
+    /**
+     * 租户ID(tenant_info.id)
+     */
+    private Long tenantId;
 
-    /** 售价(元/分钟) */
-    private BigDecimal price;
+    /**
+     * 售价(元/分钟)
+     */
+    private BigDecimal salePrice;
 
-    /** 优先级 */
+    /**
+     * 优先级
+     */
     private Integer priority;
 
-    /** 是否主线路 1是 0否 */
+    /**
+     * 是否主线路 1是 0否
+     */
     private Integer isPrimary;
 
-    /** 是否允许手动选择 1允许 0禁止 */
-    private Integer allowManual;
-
-    /** 状态 1启用 0禁用 */
+    /**
+     * 状态 1启用 0禁用
+     */
     private Integer status;
 
-    /** 创建时间 */
+    /**
+     * 创建时间
+     */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date createTime;
 
-    /** 租户名(非表字段,关联查询用) */
-    private String companyName;
-
-    /** 接口名(非表字段,关联查询用) */
+    /**
+     * 接口名(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private String apiName;
 
-    /** 成本价(非表字段,关联查询用) */
+    /**
+     * 成本价(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private BigDecimal costPrice;
 
-    /** 接口类型(非表字段,关联查询用) */
+    /**
+     * 接口类型(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private Integer apiType;
 
-    /** 服务商(非表字段,关联查询用) */
+    /**
+     * 服务商(非表字段,关联 company_voice_api 查询)
+     */
+    @TableField(exist = false)
     private String provider;
 
-    public Long getId() {
-        return id;
-    }
-
-    public void setId(Long id) {
-        this.id = id;
-    }
-
-    public Long getApiId() {
-        return apiId;
-    }
-
-    public void setApiId(Long apiId) {
-        this.apiId = apiId;
-    }
-
-    public Long getCompanyId() {
-        return companyId;
-    }
-
-    public void setCompanyId(Long companyId) {
-        this.companyId = companyId;
-    }
-
-    public Integer getStatus() {
-        return status;
-    }
-
-    public void setStatus(Integer status) {
-        this.status = status;
-    }
-
-    @Override
-    public Date getCreateTime() {
-        return createTime;
-    }
-
-    @Override
-    public void setCreateTime(Date createTime) {
-        this.createTime = createTime;
-    }
-
-    public String getCompanyName() {
-        return companyName;
-    }
-
-    public void setCompanyName(String companyName) {
-        this.companyName = companyName;
-    }
-
-    public String getApiName() {
-        return apiName;
-    }
-
-    public void setApiName(String apiName) {
-        this.apiName = apiName;
-    }
-
-    public BigDecimal getPrice() {
-        return price;
-    }
-
-    public void setPrice(BigDecimal price) {
-        this.price = price;
-    }
-
-    public Integer getPriority() {
-        return priority;
-    }
-
-    public void setPriority(Integer priority) {
-        this.priority = priority;
-    }
-
-    public Integer getIsPrimary() {
-        return isPrimary;
-    }
-
-    public void setIsPrimary(Integer isPrimary) {
-        this.isPrimary = isPrimary;
-    }
-
-    public Integer getAllowManual() {
-        return allowManual;
-    }
-
-    public void setAllowManual(Integer allowManual) {
-        this.allowManual = allowManual;
-    }
-
-    public BigDecimal getCostPrice() {
-        return costPrice;
-    }
+    /**
+     * 租户编码(冗余字段,关联 tenant_info 补全)
+     */
+    private String tenantCode;
 
-    public void setCostPrice(BigDecimal costPrice) {
-        this.costPrice = costPrice;
-    }
+    /**
+     * 租户名称(冗余字段,关联 tenant_info 补全)
+     */
+    private String tenantName;
+    /**
+     * 是否可选
+     */
+    private String selectable;
 
-    public Integer getApiType() {
-        return apiType;
-    }
-
-    public void setApiType(Integer apiType) {
-        this.apiType = apiType;
-    }
-
-    public String getProvider() {
-        return provider;
-    }
-
-    public void setProvider(String provider) {
-        this.provider = provider;
-    }
 }

+ 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;
+}

+ 11 - 2
fs-service/src/main/java/com/fs/company/domain/LobsterEventAudit.java

@@ -5,8 +5,12 @@ 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 {
@@ -15,11 +19,16 @@ public class LobsterEventAudit {
     private Long id;
     private Long companyId;
     private Long instanceId;
-    private String nodeType;
+    private String workflowId;
+    private String nodeKey;
     private String nodeName;
+    private String nodeType;
     private String nodeJson;
     private String insertAt;
-    private String reason;
+    private String insertRefNode;
+    private String decisionEngine;
+    private BigDecimal decisionScore;
+    private String decisionReason;
     private String status;
     private String auditBy;
     private String auditComment;

+ 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;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyLobsterTagUserRelMapper.java

@@ -2,9 +2,11 @@ package com.fs.company.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.CompanyLobsterTagUserRel;
+import org.apache.ibatis.annotations.MapKey;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
 public interface CompanyLobsterTagUserRelMapper extends BaseMapper<CompanyLobsterTagUserRel> {
 
@@ -12,4 +14,6 @@ public interface CompanyLobsterTagUserRelMapper extends BaseMapper<CompanyLobste
 
     void updateBatchRelBybinding(@Param("id") Long id,@Param("flag") Integer unDelFlag);
 
+    @MapKey("id")
+    Map<String,String> selectLobsterTagsByExId(@Param("userIds") List<Long> userIds, @Param("userId") Long userId, @Param("companyId") Long companyId);
 }

+ 1 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiMapper.java

@@ -59,6 +59,6 @@ public interface CompanyVoiceApiMapper
      * @return 结果
      */
     public int deleteCompanyVoiceApiByIds(Long[] apiIds);
-    @Select("select count(1) from company_voice_api")
+    @Select("select count(1) from company_voice_api where (is_del = 0 or is_del is null)")
     Integer selectCompanyVoiceApiCount();
 }

+ 32 - 58
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceApiTenantMapper.java

@@ -14,64 +14,38 @@ import java.util.List;
  */
 public interface CompanyVoiceApiTenantMapper
 {
-    /**
-     * 查询分配关系
-     */
-    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
-
-    /**
-     * 按接口ID+租户ID查询分配关系
-     */
-    public CompanyVoiceApiTenant selectByApiAndCompany(@Param("apiId") Long apiId, @Param("companyId") Long companyId);
-
-    /**
-     * 查询接口已分配的租户列表(含租户名)
-     */
-    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
-
-    /**
-     * 查询租户已分配的接口列表(含接口名)
-     */
-    public List<CompanyVoiceApiTenant> selectApisByCompanyId(Long companyId);
-
-    /**
-     * 查询分配关系列表
-     */
-    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
-
-    /**
-     * 新增分配关系
-     */
-    public int insertCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
-
-    /**
-     * 批量新增分配关系
-     */
-    public int batchInsertCompanyVoiceApiTenant(@Param("list") List<CompanyVoiceApiTenant> list);
-
-    /**
-     * 修改分配关系
-     */
-    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
-
-    /**
-     * 删除分配关系
-     */
-    public int deleteCompanyVoiceApiTenantById(Long id);
-
-    /**
-     * 按接口ID+租户ID删除分配关系
-     */
-    public int deleteByApiAndCompany(@Param("apiId") Long apiId, @Param("companyId") Long companyId);
-
-    /**
-     * 按接口ID删除所有分配关系
-     */
-    public int deleteByApiId(Long apiId);
-
-    /**
-     * 查询接口已分配的租户数量
-     */
+    CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
+
+    CompanyVoiceApiTenant selectByApiAndTenant(@Param("apiId") Long apiId, @Param("tenantId") Long tenantId);
+
+    List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
+
+    List<CompanyVoiceApiTenant> selectApisByTenantId(Long tenantId);
+
+    List<CompanyVoiceApiTenant> selectEnabledApisByTenantId(Long tenantId);
+
+    List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
+
+    int insertCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    int batchInsertCompanyVoiceApiTenant(@Param("list") List<CompanyVoiceApiTenant> list);
+
+    int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    int deleteCompanyVoiceApiTenantById(Long id);
+
+    int deleteByApiAndTenant(@Param("apiId") Long apiId, @Param("tenantId") Long tenantId);
+
+    int deleteByApiId(Long apiId);
+
+    int disableByApiId(Long apiId);
+
+    int disableByApiIds(@Param("apiIds") Long[] apiIds);
+
+    int batchUpdatePricing(@Param("ids") List<Long> ids, @Param("data") CompanyVoiceApiTenant data);
+
+    int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
+
     @Select("SELECT COUNT(1) FROM company_voice_api_tenant WHERE api_id = #{apiId} AND status = 1")
     Integer selectTenantCountByApiId(Long apiId);
 }

+ 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);
+}

+ 29 - 6
fs-service/src/main/java/com/fs/company/mapper/LobsterApiRegistryMapper.java

@@ -1,13 +1,36 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterApiRegistry;
-import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
-@Mapper
-public interface LobsterApiRegistryMapper extends BaseMapper<LobsterApiRegistry> {
+/**
+ * 龙虾外部 API 注册 Mapper(lobster_api_registry 表)
+ */
+public interface LobsterApiRegistryMapper {
 
-    List<LobsterApiRegistry> selectEnabled();
+    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 - 4
fs-service/src/main/java/com/fs/company/mapper/LobsterComplianceRuleMapper.java

@@ -12,18 +12,16 @@ import java.util.List;
 @Mapper
 public interface LobsterComplianceRuleMapper extends BaseMapper<LobsterComplianceRule> {
 
-    @Select("SELECT * FROM lobster_compliance_rule WHERE company_id = #{companyId}")
     List<LobsterComplianceRule> selectByCompanyId(@Param("companyId") Long companyId);
 
-    @Select("SELECT * FROM lobster_compliance_rule WHERE company_id = #{companyId} AND enabled = 1")
     List<LobsterComplianceRule> selectEnabledByCompanyId(@Param("companyId") Long companyId);
 
-    @Select("SELECT * FROM lobster_compliance_rule WHERE id = #{id} AND company_id = #{companyId}")
     LobsterComplianceRule selectByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
 
     @Select("SELECT pattern FROM lobster_compliance_rule WHERE company_id = #{companyId} AND rule_type = 'sensitive' AND enabled = 1")
     List<String> selectSensitivePatterns(@Param("companyId") Long companyId);
 
-    @Update("UPDATE lobster_compliance_rule SET deleted = 1 WHERE id = #{id} AND company_id = #{companyId}")
     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);
+}

+ 21 - 28
fs-service/src/main/java/com/fs/company/mapper/LobsterDialogueStateMapper.java

@@ -1,31 +1,24 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterDialogueState;
-import org.apache.ibatis.annotations.*;
-
-@Mapper
-public interface LobsterDialogueStateMapper extends BaseMapper<LobsterDialogueState> {
-
-    @Select("SELECT state_json FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode} AND company_id = #{companyId}")
-    String selectStateJson(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode, @Param("companyId") Long companyId);
-
-    @Select("SELECT state_json FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode}")
-    String selectStateJsonNoCompany(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode);
-
-    @Delete("DELETE FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode} AND company_id = #{companyId}")
-    int deleteByInstanceAndNode(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode, @Param("companyId") Long companyId);
-
-    @Delete("DELETE FROM lobster_dialogue_state WHERE instance_id = #{instanceId} AND node_code = #{nodeCode}")
-    int deleteByInstanceAndNodeNoCompany(@Param("instanceId") Long instanceId, @Param("nodeCode") String nodeCode);
-
-    @Insert("INSERT INTO lobster_dialogue_state (company_id, instance_id, node_code, state_json, update_time) " +
-            "VALUES (#{companyId}, #{instanceId}, #{nodeCode}, #{stateJson}, NOW()) " +
-            "ON DUPLICATE KEY UPDATE state_json = #{stateJson}, update_time = NOW()")
-    int upsert(LobsterDialogueState entity);
-
-    @Insert("INSERT INTO lobster_dialogue_state (instance_id, node_code, state_json, update_time) " +
-            "VALUES (#{instanceId}, #{nodeCode}, #{stateJson}, NOW()) " +
-            "ON DUPLICATE KEY UPDATE state_json = #{stateJson}, update_time = NOW()")
-    int upsertNoCompany(LobsterDialogueState entity);
+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);
 }

+ 17 - 12
fs-service/src/main/java/com/fs/company/mapper/LobsterEventAuditMapper.java

@@ -1,23 +1,28 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.company.domain.LobsterEventAudit;
-import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
 
-@Mapper
-public interface LobsterEventAuditMapper extends BaseMapper<LobsterEventAudit> {
+/**
+ * 龙虾事件节点审核 Mapper — 表: lobster_event_node_audit
+ */
+public interface LobsterEventAuditMapper {
 
-    List<LobsterEventAudit> selectListByCompanyIdAndStatus(@Param("companyId") Long companyId,
-                                                            @Param("status") String status,
-                                                            @Param("offset") int offset,
-                                                            @Param("limit") int limit);
+    List<LobsterEventAudit> selectList(@Param("companyId") Long companyId,
+                                        @Param("status") String status,
+                                        @Param("offset") int offset,
+                                        @Param("limit") int limit);
 
-    long countByCompanyIdAndStatus(@Param("companyId") Long companyId,
-                                   @Param("status") String status);
+    long countByStatus(@Param("companyId") Long companyId, @Param("status") String status);
 
-    LobsterEventAudit selectByIdAndStatus(@Param("id") Long id,
-                                           @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);
 }

+ 88 - 5
fs-service/src/main/java/com/fs/company/mapper/LobsterEvolutionConfigMapper.java

@@ -1,9 +1,92 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterEvolutionConfig;
-import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
-@Mapper
-public interface LobsterEvolutionConfigMapper extends BaseMapper<LobsterEvolutionConfig> {
+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();
+}

+ 11 - 5
fs-service/src/main/java/com/fs/company/mapper/LobsterHandoffEventMapper.java

@@ -1,9 +1,15 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterHandoffEvent;
-import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
-@Mapper
-public interface LobsterHandoffEventMapper extends BaseMapper<LobsterHandoffEvent> {
+/**
+ * 龙虾转人工事件 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);
+}

+ 22 - 15
fs-service/src/main/java/com/fs/company/mapper/LobsterLearningCorpusMapper.java

@@ -1,28 +1,35 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterLearningCorpus;
-import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
-@Mapper
-public interface LobsterLearningCorpusMapper extends BaseMapper<LobsterLearningCorpus> {
+/**
+ * 销冠语料库 Mapper(lobster_learning_corpus 表)
+ */
+public interface LobsterLearningCorpusMapper {
 
-    List<LobsterLearningCorpus> selectListByCompanyId(@Param("companyId") Long companyId,
-                                                       @Param("scenario") String scenario,
-                                                       @Param("status") String status);
+    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<LobsterLearningCorpus> selectRawByCompanyId(@Param("companyId") Long companyId,
-                                                      @Param("limit") int limit);
+    List<Map<String, Object>> selectByScenario(@Param("companyId") Long companyId,
+                                                @Param("scenario") String scenario,
+                                                @Param("limit") int limit);
 
-    List<LobsterLearningCorpus> 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("limit") int limit);
+                     @Param("count") int count);
 
-    int incrementUsage(@Param("id") Long id);
+    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);
 }

+ 49 - 27
fs-service/src/main/java/com/fs/company/mapper/LobsterPendingKnowledgeMapper.java

@@ -1,32 +1,54 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterPendingKnowledge;
-import org.apache.ibatis.annotations.*;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
-
-@Mapper
-public interface LobsterPendingKnowledgeMapper extends BaseMapper<LobsterPendingKnowledge> {
-
-    @Select("SELECT * FROM lobster_pending_knowledge WHERE company_id = #{companyId} AND audit_status = 'pending'")
-    List<LobsterPendingKnowledge> selectPendingByCompanyId(@Param("companyId") Long companyId);
-
-    @Select("SELECT COUNT(*) FROM lobster_pending_knowledge WHERE company_id = #{companyId} AND audit_status = 'pending'")
-    int countPendingByCompanyId(@Param("companyId") Long companyId);
-
-    @Select("SELECT * FROM lobster_pending_knowledge WHERE id = #{id} AND company_id = #{companyId}")
-    LobsterPendingKnowledge selectByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
-
-    @Update("UPDATE lobster_pending_knowledge SET question = #{question}, answer = #{answer}, update_by = #{updateBy}, update_time = NOW() WHERE id = #{id} AND company_id = #{companyId}")
-    int updateContent(@Param("id") Long id, @Param("companyId") Long companyId, @Param("question") String question, @Param("answer") String answer, @Param("updateBy") String updateBy);
-
-    @Update("UPDATE lobster_pending_knowledge SET audit_status = 'approved', auditor = #{auditor}, audit_time = NOW(), update_time = NOW() WHERE id = #{id} AND company_id = #{companyId}")
-    int approve(@Param("id") Long id, @Param("companyId") Long companyId, @Param("auditor") String auditor);
-
-    @Update("UPDATE lobster_pending_knowledge SET audit_status = 'rejected', audit_comment = #{comment}, auditor = #{auditor}, audit_time = NOW(), update_time = NOW() WHERE id = #{id} AND company_id = #{companyId} AND audit_status = 'pending'")
-    int reject(@Param("id") Long id, @Param("companyId") Long companyId, @Param("auditor") String auditor, @Param("comment") String comment);
-
-    @Delete("DELETE FROM lobster_pending_knowledge WHERE id = #{id} AND company_id = #{companyId}")
-    int deleteByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+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();
+}

+ 17 - 7
fs-service/src/main/java/com/fs/company/mapper/LobsterSensitiveWordMapper.java

@@ -1,16 +1,26 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterSensitiveWord;
-import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
-@Mapper
-public interface LobsterSensitiveWordMapper extends BaseMapper<LobsterSensitiveWord> {
+/**
+ * 龙虾敏感词 Mapper(lobster_sensitive_word 表)
+ */
+public interface LobsterSensitiveWordMapper {
 
-    List<LobsterSensitiveWord> selectByCompanyId(@Param("companyId") Long companyId);
+    int insert(@Param("companyId") Long companyId,
+               @Param("word") String word,
+               @Param("level") String level,
+               @Param("replacement") String replacement,
+               @Param("enabled") Integer enabled);
 
-    int softDeleteById(@Param("id") Long id);
+    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);
+}

+ 20 - 4
fs-service/src/main/java/com/fs/company/mapper/LobsterSystemPromptMapper.java

@@ -27,8 +27,24 @@ public interface LobsterSystemPromptMapper {
 
     List<String> selectCategories();
 
-    /**
-     * 查询所有启用的提示词(用于缓存全量加载)
-     */
-    List<LobsterSystemPrompt> selectEnabledOrdered();
+    /** 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();
+}

+ 23 - 13
fs-service/src/main/java/com/fs/company/mapper/LobsterUserPreferenceMapper.java

@@ -1,22 +1,32 @@
 package com.fs.company.mapper;
 
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.fs.company.domain.LobsterUserPreference;
-import org.apache.ibatis.annotations.*;
+import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Map;
 
-@Mapper
-public interface LobsterUserPreferenceMapper extends BaseMapper<LobsterUserPreference> {
+/**
+ * 龙虾用户偏好 Mapper(lobster_user_preference 表)
+ * 千人千面个性化引擎数据源
+ */
+public interface LobsterUserPreferenceMapper {
 
-    @Select("SELECT * FROM lobster_user_preference WHERE company_id = #{companyId} AND external_user_id = #{externalUserId}")
-    List<LobsterUserPreference> selectByUserId(@Param("companyId") Long companyId, @Param("externalUserId") String externalUserId);
+    /** 保存/更新偏好快照 */
+    int upsert(@Param("companyId") Long companyId,
+               @Param("externalUserId") String externalUserId,
+               @Param("snapshotJson") String snapshotJson);
 
-    @Select("SELECT * FROM lobster_user_preference WHERE company_id = #{companyId} AND external_user_id = #{externalUserId} AND preference_type = #{preferenceType}")
-    LobsterUserPreference selectByUserIdAndType(@Param("companyId") Long companyId, @Param("externalUserId") String externalUserId, @Param("preferenceType") String preferenceType);
+    /** 读取渠道偏好覆盖消息模板 */
+    String selectMessageOverride(@Param("companyId") Long companyId,
+                                  @Param("externalUserId") String externalUserId);
 
-    @Insert("INSERT INTO lobster_user_preference (company_id, external_user_id, preference_type, preference_value, update_time) " +
-            "VALUES (#{companyId}, #{externalUserId}, #{preferenceType}, #{preferenceValue}, NOW()) " +
-            "ON DUPLICATE KEY UPDATE preference_value = #{preferenceValue}, update_time = NOW()")
-    int upsert(LobsterUserPreference entity);
+    /** 查询用户偏好 */
+    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);
+}

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

@@ -54,4 +54,6 @@ public interface ICompanyTagTemplateBindingService {
      * 批量添加龙虾标签给企微客户
      */
     AjaxResult batchBindLobsterTag(Long companyId, String userName, String qwCorpId, List<Long> externalContactIds, List<String> tagCodes,Long companyUserId);
+
+    AjaxResult lobsterTags(List<Long> userIds, Long userId, Long companyId);
 }

+ 22 - 32
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceApiTenantService.java

@@ -12,53 +12,43 @@ import java.util.List;
  */
 public interface ICompanyVoiceApiTenantService
 {
-    /**
-     * 查询分配关系
-     */
-    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
+    CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id);
 
-    /**
-     * 按接口ID+租户ID查询分配关系
-     */
-    public CompanyVoiceApiTenant selectByApiAndCompany(Long apiId, Long companyId);
+    CompanyVoiceApiTenant selectByApiAndTenant(Long apiId, Long tenantId);
 
-    /**
-     * 查询接口已分配的租户列表(含租户名)
-     */
-    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
+    List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId);
 
-    /**
-     * 查询租户已分配的接口列表(含接口名,仅启用的)
-     */
-    public List<CompanyVoiceApiTenant> selectEnabledApisByCompanyId(Long companyId);
+    List<CompanyVoiceApiTenant> selectEnabledApisByTenantId(Long tenantId);
 
-    /**
-     * 查询分配关系列表
-     */
-    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
+    List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param);
 
-    /**
-     * 分配接口给租户
-     */
-    public int assignTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+    int assignTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+
+    int batchAssignTenants(Long apiId, List<CompanyVoiceApiTenant> assignList);
+
+    int unassignTenant(Long apiId, Long tenantId);
 
     /**
-     * 批量分配接口给租户
+     * 停用接口下所有已启用的租户分配关系
      */
-    public int batchAssignTenants(Long apiId, List<Long> companyIds);
+    int disableTenantsByApiId(Long apiId);
 
     /**
-     * 取消分配
+     * 批量停用接口下所有已启用的租户分配关系
      */
-    public int unassignTenant(Long apiId, Long companyId);
+    int disableTenantsByApiIds(Long[] apiIds);
+
+    int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
 
     /**
-     * 修改分配关系状态
+     * 批量更新定价配置(仅更新非空字段)
      */
-    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant);
+    int batchUpdatePricing(List<Long> ids, CompanyVoiceApiTenant pricing);
 
     /**
-     * 查询接口已分配的租户数量(启用状态)
+     * 批量更新状态
      */
-    public Integer selectTenantCountByApiId(Long apiId);
+    int batchUpdateStatus(List<Long> ids, Integer status);
+
+    Integer selectTenantCountByApiId(Long apiId);
 }

+ 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;
     }
 

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyTagTemplateBindingServiceImpl.java

@@ -409,6 +409,12 @@ public class CompanyTagTemplateBindingServiceImpl implements ICompanyTagTemplate
         return AjaxResult.success("已为 " + externalContactIds.size() + " 个客户添加 " + targetBindings.size() + " 个龙虾标签");
     }
 
+    @Override
+    public AjaxResult lobsterTags(List<Long> userIds, Long userId, Long companyId) {
+        return AjaxResult.success(companyLobsterTagUserRelMapper.selectLobsterTagsByExId( userIds,  userId,  companyId));
+
+    }
+
     private LocalDateTime getSendTimeNode(CompanyWorkflowLobsterNode node) {
         Integer days = Integer.valueOf(node.getNodeCode().substring(4));
         LocalDate date = LocalDate.now().plusDays(days);

+ 39 - 4
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiServiceImpl.java

@@ -1,11 +1,15 @@
 package com.fs.company.service.impl;
 
-import java.util.List;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import com.fs.company.mapper.CompanyVoiceApiMapper;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.CompanyVoiceApi;
+import com.fs.company.mapper.CompanyVoiceApiMapper;
 import com.fs.company.service.ICompanyVoiceApiService;
+import com.fs.company.service.ICompanyVoiceApiTenantService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
 
 /**
  * 呼叫接口Service业务层处理
@@ -19,6 +23,9 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
     @Autowired
     private CompanyVoiceApiMapper companyVoiceApiMapper;
 
+    @Autowired
+    private ICompanyVoiceApiTenantService companyVoiceApiTenantService;
+
     /**
      * 查询呼叫接口
      * 
@@ -52,6 +59,7 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
     @Override
     public int insertCompanyVoiceApi(CompanyVoiceApi companyVoiceApi)
     {
+        prepareForSave(companyVoiceApi);
         return companyVoiceApiMapper.insertCompanyVoiceApi(companyVoiceApi);
     }
 
@@ -64,9 +72,24 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
     @Override
     public int updateCompanyVoiceApi(CompanyVoiceApi companyVoiceApi)
     {
+        prepareForSave(companyVoiceApi);
         return companyVoiceApiMapper.updateCompanyVoiceApi(companyVoiceApi);
     }
 
+    /** 保存前补全默认值(数据写入 account/password/api_url/dialog_url 等新字段,不使用 apiJson) */
+    private void prepareForSave(CompanyVoiceApi api) {
+        if (api == null) {
+            return;
+        }
+        if (StringUtils.isEmpty(api.getProvider())) {
+            api.setProvider("platform");
+        }
+        if (api.getIsDel() == null) {
+            api.setIsDel(0);
+        }
+//        api.setApiJson(null);
+    }
+
     /**
      * 批量删除呼叫接口
      * 
@@ -74,8 +97,14 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
      * @return 结果
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int deleteCompanyVoiceApiByIds(Long[] apiIds)
     {
+        if (apiIds == null || apiIds.length == 0)
+        {
+            return 0;
+        }
+        companyVoiceApiTenantService.disableTenantsByApiIds(apiIds);
         return companyVoiceApiMapper.deleteCompanyVoiceApiByIds(apiIds);
     }
 
@@ -86,8 +115,14 @@ public class CompanyVoiceApiServiceImpl implements ICompanyVoiceApiService
      * @return 结果
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int deleteCompanyVoiceApiById(Long apiId)
     {
+        if (apiId == null)
+        {
+            return 0;
+        }
+        companyVoiceApiTenantService.disableTenantsByApiId(apiId);
         return companyVoiceApiMapper.deleteCompanyVoiceApiById(apiId);
     }
 

+ 218 - 125
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceApiTenantServiceImpl.java

@@ -1,125 +1,218 @@
-package com.fs.company.service.impl;
-
-import com.fs.company.domain.CompanyVoiceApiTenant;
-import com.fs.company.mapper.CompanyVoiceApiTenantMapper;
-import com.fs.company.service.ICompanyVoiceApiTenantService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 通话接口-租户分配关系Service实现
- *
- * @author fs
- * @date 2026-05-21
- */
-@Service
-public class CompanyVoiceApiTenantServiceImpl implements ICompanyVoiceApiTenantService
-{
-    @Autowired
-    private CompanyVoiceApiTenantMapper companyVoiceApiTenantMapper;
-
-    @Override
-    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id)
-    {
-        return companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantById(id);
-    }
-
-    @Override
-    public CompanyVoiceApiTenant selectByApiAndCompany(Long apiId, Long companyId)
-    {
-        return companyVoiceApiTenantMapper.selectByApiAndCompany(apiId, companyId);
-    }
-
-    @Override
-    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId)
-    {
-        return companyVoiceApiTenantMapper.selectTenantsByApiId(apiId);
-    }
-
-    @Override
-    public List<CompanyVoiceApiTenant> selectEnabledApisByCompanyId(Long companyId)
-    {
-        CompanyVoiceApiTenant param = new CompanyVoiceApiTenant();
-        param.setCompanyId(companyId);
-        param.setStatus(1);
-        return companyVoiceApiTenantMapper.selectApisByCompanyId(companyId);
-    }
-
-    @Override
-    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param)
-    {
-        return companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantList(param);
-    }
-
-    @Override
-    public int assignTenant(CompanyVoiceApiTenant companyVoiceApiTenant)
-    {
-        // 检查是否已存在分配关系
-        CompanyVoiceApiTenant existing = companyVoiceApiTenantMapper.selectByApiAndCompany(
-                companyVoiceApiTenant.getApiId(), companyVoiceApiTenant.getCompanyId());
-        if (existing != null)
-        {
-            // 已存在则更新状态为启用
-            existing.setStatus(1);
-            return companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(existing);
-        }
-        if (companyVoiceApiTenant.getStatus() == null)
-        {
-            companyVoiceApiTenant.setStatus(1);
-        }
-        return companyVoiceApiTenantMapper.insertCompanyVoiceApiTenant(companyVoiceApiTenant);
-    }
-
-    @Override
-    public int batchAssignTenants(Long apiId, List<Long> companyIds)
-    {
-        if (companyIds == null || companyIds.isEmpty())
-        {
-            return 0;
-        }
-        List<CompanyVoiceApiTenant> toInsert = new ArrayList<>();
-        for (Long companyId : companyIds)
-        {
-            CompanyVoiceApiTenant existing = companyVoiceApiTenantMapper.selectByApiAndCompany(apiId, companyId);
-            if (existing == null)
-            {
-                CompanyVoiceApiTenant rel = new CompanyVoiceApiTenant();
-                rel.setApiId(apiId);
-                rel.setCompanyId(companyId);
-                rel.setStatus(1);
-                toInsert.add(rel);
-            }
-            else if (existing.getStatus() == null || existing.getStatus() == 0)
-            {
-                existing.setStatus(1);
-                companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(existing);
-            }
-        }
-        if (!toInsert.isEmpty())
-        {
-            return companyVoiceApiTenantMapper.batchInsertCompanyVoiceApiTenant(toInsert);
-        }
-        return 0;
-    }
-
-    @Override
-    public int unassignTenant(Long apiId, Long companyId)
-    {
-        return companyVoiceApiTenantMapper.deleteByApiAndCompany(apiId, companyId);
-    }
-
-    @Override
-    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant)
-    {
-        return companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(companyVoiceApiTenant);
-    }
-
-    @Override
-    public Integer selectTenantCountByApiId(Long apiId)
-    {
-        return companyVoiceApiTenantMapper.selectTenantCountByApiId(apiId);
-    }
-}
+package com.fs.company.service.impl;

+

+import com.fs.common.utils.StringUtils;

+import com.fs.company.domain.CompanyVoiceApiTenant;

+import com.fs.company.mapper.CompanyVoiceApiTenantMapper;

+import com.fs.company.service.ICompanyVoiceApiTenantService;

+import com.fs.tenant.domain.TenantInfo;

+import com.fs.tenant.mapper.TenantInfoMapper;

+import org.springframework.beans.factory.annotation.Autowired;

+import org.springframework.stereotype.Service;

+import org.springframework.transaction.annotation.Transactional;

+

+import java.util.ArrayList;

+import java.util.List;

+

+/**

+ * 通话接口-租户分配关系Service实现

+ *

+ * @author fs

+ * @date 2026-05-21

+ */

+@Service

+public class CompanyVoiceApiTenantServiceImpl implements ICompanyVoiceApiTenantService

+{

+    @Autowired

+    private CompanyVoiceApiTenantMapper companyVoiceApiTenantMapper;

+

+    @Autowired

+    private TenantInfoMapper tenantInfoMapper;

+

+    @Override

+    public CompanyVoiceApiTenant selectCompanyVoiceApiTenantById(Long id)

+    {

+        return companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantById(id);

+    }

+

+    @Override

+    public CompanyVoiceApiTenant selectByApiAndTenant(Long apiId, Long tenantId)

+    {

+        return companyVoiceApiTenantMapper.selectByApiAndTenant(apiId, tenantId);

+    }

+

+    @Override

+    public List<CompanyVoiceApiTenant> selectTenantsByApiId(Long apiId)

+    {

+        return companyVoiceApiTenantMapper.selectTenantsByApiId(apiId);

+    }

+

+    @Override

+    public List<CompanyVoiceApiTenant> selectEnabledApisByTenantId(Long tenantId)

+    {

+        return companyVoiceApiTenantMapper.selectEnabledApisByTenantId(tenantId);

+    }

+

+    @Override

+    public List<CompanyVoiceApiTenant> selectCompanyVoiceApiTenantList(CompanyVoiceApiTenant param)

+    {

+        return companyVoiceApiTenantMapper.selectCompanyVoiceApiTenantList(param);

+    }

+

+    @Override

+    public int assignTenant(CompanyVoiceApiTenant companyVoiceApiTenant)

+    {

+        fillTenantFields(companyVoiceApiTenant);

+        CompanyVoiceApiTenant existing = companyVoiceApiTenantMapper.selectByApiAndTenant(

+                companyVoiceApiTenant.getApiId(), companyVoiceApiTenant.getTenantId());

+        if (existing != null)

+        {

+            existing.setStatus(1);

+            copyTenantFields(companyVoiceApiTenant, existing);

+            return companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(existing);

+        }

+        if (companyVoiceApiTenant.getStatus() == null)

+        {

+            companyVoiceApiTenant.setStatus(1);

+        }

+        return companyVoiceApiTenantMapper.insertCompanyVoiceApiTenant(companyVoiceApiTenant);

+    }

+

+    @Override

+    public int batchAssignTenants(Long apiId, List<CompanyVoiceApiTenant> assignList)

+    {

+        if (apiId == null || assignList == null || assignList.isEmpty())

+        {

+            return 0;

+        }

+        List<CompanyVoiceApiTenant> toInsert = new ArrayList<>();

+        for (CompanyVoiceApiTenant item : assignList)

+        {

+            if (item == null || item.getTenantId() == null)

+            {

+                continue;

+            }

+            item.setApiId(apiId);

+            fillTenantFields(item);

+            CompanyVoiceApiTenant existing = companyVoiceApiTenantMapper.selectByApiAndTenant(apiId, item.getTenantId());

+            if (existing == null)

+            {

+                if (item.getStatus() == null)

+                {

+                    item.setStatus(1);

+                }

+                toInsert.add(item);

+            }

+            else if (existing.getStatus() == null || existing.getStatus() == 0)

+            {

+                existing.setStatus(1);

+                copyTenantFields(item, existing);

+                companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(existing);

+            }

+        }

+        if (!toInsert.isEmpty())

+        {

+            return companyVoiceApiTenantMapper.batchInsertCompanyVoiceApiTenant(toInsert);

+        }

+        return 0;

+    }

+

+    private void fillTenantFields(CompanyVoiceApiTenant rel)

+    {

+        if (rel == null || rel.getTenantId() == null

+                || (!StringUtils.isEmpty(rel.getTenantCode()) && !StringUtils.isEmpty(rel.getTenantName())))

+        {

+            return;

+        }

+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(String.valueOf(rel.getTenantId()));

+        if (tenantInfo != null)

+        {

+            if (StringUtils.isEmpty(rel.getTenantCode()))

+            {

+                rel.setTenantCode(tenantInfo.getTenantCode());

+            }

+            if (StringUtils.isEmpty(rel.getTenantName()))

+            {

+                rel.setTenantName(tenantInfo.getTenantName());

+            }

+        }

+    }

+

+    private void copyTenantFields(CompanyVoiceApiTenant source, CompanyVoiceApiTenant target)

+    {

+        if (source == null || target == null)

+        {

+            return;

+        }

+        target.setTenantCode(source.getTenantCode());

+        target.setTenantName(source.getTenantName());

+    }

+

+    @Override

+    public int unassignTenant(Long apiId, Long tenantId)

+    {

+        return companyVoiceApiTenantMapper.deleteByApiAndTenant(apiId, tenantId);

+    }

+

+    @Override

+    public int disableTenantsByApiId(Long apiId)

+    {

+        if (apiId == null)

+        {

+            return 0;

+        }

+        return companyVoiceApiTenantMapper.disableByApiId(apiId);

+    }

+

+    @Override

+    public int disableTenantsByApiIds(Long[] apiIds)

+    {

+        if (apiIds == null || apiIds.length == 0)

+        {

+            return 0;

+        }

+        return companyVoiceApiTenantMapper.disableByApiIds(apiIds);

+    }

+

+    @Override

+    public int updateCompanyVoiceApiTenant(CompanyVoiceApiTenant companyVoiceApiTenant)

+    {

+        return companyVoiceApiTenantMapper.updateCompanyVoiceApiTenant(companyVoiceApiTenant);

+    }

+

+    @Override

+    @Transactional(rollbackFor = Exception.class)

+    public int batchUpdatePricing(List<Long> ids, CompanyVoiceApiTenant pricing)

+    {

+        if (ids == null || ids.isEmpty() || pricing == null)

+        {

+            return 0;

+        }

+        boolean hasField = pricing.getSalePrice() != null

+                || pricing.getPriority() != null

+                || pricing.getIsPrimary() != null

+                || pricing.getSelectable() != null;

+        if (!hasField)

+        {

+            return 0;

+        }

+        return companyVoiceApiTenantMapper.batchUpdatePricing(ids, pricing);

+    }

+

+    @Override

+    @Transactional(rollbackFor = Exception.class)

+    public int batchUpdateStatus(List<Long> ids, Integer status)

+    {

+        if (ids == null || ids.isEmpty() || status == null)

+        {

+            return 0;

+        }

+        return companyVoiceApiTenantMapper.batchUpdateStatus(ids, status);

+    }

+

+    @Override

+    public Integer selectTenantCountByApiId(Long apiId)

+    {

+        return companyVoiceApiTenantMapper.selectTenantCountByApiId(apiId);

+    }

+}

+

+ 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);
+}

+ 8 - 9
fs-service/src/main/java/com/fs/company/service/workflow/ILobsterEventAuditService.java

@@ -2,19 +2,18 @@ package com.fs.company.service.workflow;
 
 import com.fs.company.domain.LobsterEventAudit;
 
-import java.util.List;
+import java.util.Map;
 
+/**
+ * 龙虾事件节点审核 Service
+ */
 public interface ILobsterEventAuditService {
 
-    List<LobsterEventAudit> selectListByCompanyIdAndStatus(Long companyId, String status, int page, int size);
+    Map<String, Object> listAudits(String status, int page, int size, Long companyId);
 
-    long countByCompanyIdAndStatus(Long companyId, String status);
+    LobsterEventAudit getById(Long id);
 
-    LobsterEventAudit selectByIdAndStatus(Long id, String status);
+    boolean approve(Long id, String username);
 
-    void approve(Long id, String auditBy, String auditComment);
-
-    void reject(Long id, String auditBy, String auditComment);
-
-    LobsterEventAudit selectById(Long id);
+    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);
 }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff