boss 2 tygodni temu
rodzic
commit
ac1c45cc3c
32 zmienionych plików z 2595 dodań i 284 usunięć
  1. 52 7
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java
  2. 31 6
      fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreMiscController.java
  3. 20 0
      fs-admin-saas/src/main/java/com/fs/admin/sync/LobsterBridgeDataSyncService.java
  4. 0 14
      fs-admin-saas/src/main/java/com/fs/company/controller/workflow/SaasMissingApisStubController.java
  5. 74 0
      fs-admin-saas/src/main/resources/db/migration/tenant/V20260521_01__add_lobster_engine_menus.sql
  6. 28 0
      fs-admin-saas/src/main/resources/db/migration/tenant/V20260521_02__remove_adminminui_unused_menus.sql
  7. 135 0
      fs-admin/src/main/java/com/fs/admin/controller/AiModelAdminController.java
  8. 128 0
      fs-admin/src/main/java/com/fs/admin/controller/AiSceneAdminController.java
  9. 49 0
      fs-service/src/main/java/com/fs/company/domain/AdminAiModel.java
  10. 37 0
      fs-service/src/main/java/com/fs/company/domain/AdminAiScene.java
  11. 36 0
      fs-service/src/main/java/com/fs/company/domain/AdminAiSceneModel.java
  12. 30 0
      fs-service/src/main/java/com/fs/company/mapper/AdminAiModelMapper.java
  13. 26 0
      fs-service/src/main/java/com/fs/company/mapper/AdminAiSceneMapper.java
  14. 39 0
      fs-service/src/main/java/com/fs/company/mapper/AdminAiSceneModelMapper.java
  15. 129 0
      fs-service/src/main/java/com/fs/company/service/ai/AdminAiModelService.java
  16. 156 0
      fs-service/src/main/java/com/fs/company/service/ai/AdminAiSceneService.java
  17. 28 0
      fs-service/src/main/java/com/fs/company/service/ai/AiModelGateway.java
  18. 68 97
      fs-service/src/main/java/com/fs/company/service/ai/AiProviderManager.java
  19. 203 0
      fs-service/src/main/java/com/fs/company/service/ai/AiSceneDispatcher.java
  20. 439 0
      fs-service/src/main/java/com/fs/company/service/ai/MultiModelPipelineEngine.java
  21. 43 1
      fs-service/src/main/java/com/fs/company/service/llm/impl/MultiModelRouterImpl.java
  22. 116 99
      fs-service/src/main/java/com/fs/company/service/workflow/impl/MultiModelWorkflowGeneratorImpl.java
  23. 123 60
      fs-service/src/main/java/com/fs/company/service/workflow/impl/QualityScoringServiceImpl.java
  24. 95 0
      fs-service/src/main/resources/mapper/company/AdminAiModelMapper.xml
  25. 68 0
      fs-service/src/main/resources/mapper/company/AdminAiSceneMapper.xml
  26. 95 0
      fs-service/src/main/resources/mapper/company/AdminAiSceneModelMapper.xml
  27. 12 0
      sql/add_missing_admin_menu.sql
  28. 148 0
      sql/admin_ai_model_config.sql
  29. 22 0
      sql/check_fix_menu.sql
  30. 49 0
      sql/hide_adminminui_placeholder_menus.sql
  31. 95 0
      sql/lobster_menu_init.sql
  32. 21 0
      sql/update_admin_ai_model_menu.sql

+ 52 - 7
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminCompanyBridgeController.java

@@ -310,23 +310,63 @@ public class AdminCompanyBridgeController extends BaseController {
     }
 
     // ========== saasui company管理端点(角色/菜单/部门/岗位/员工/充值/统计) ==========
+    @Autowired(required = false)
+    private ICompanyRoleService companyRoleService;
+    @Autowired(required = false)
+    private ICompanyMenuService companyMenuService;
+    @Autowired(required = false)
+    private ICompanyDeptService companyDeptService;
+    @Autowired(required = false)
+    private ICompanyPostService companyPostService;
+    @Autowired(required = false)
+    private ICompanyUserService companyUserService;
+    @Autowired(required = false)
+    private ICompanyRechargeService companyRechargeService;
+
     @GetMapping("/company/role/list")
-    public TableDataInfo companyRoleList() { return getDataTable(new ArrayList<>()); }
+    public TableDataInfo companyRoleList(CompanyRole param) {
+        startPage();
+        List<CompanyRole> list = companyRoleService != null ?
+            companyRoleService.selectCompanyRoleList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
 
     @GetMapping("/company/menu/list")
-    public AjaxResult companyMenuList() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult companyMenuList(CompanyMenu param) {
+        List<CompanyMenu> list = companyMenuService != null ?
+            companyMenuService.selectCompanyMenuList(param) : new ArrayList<>();
+        return AjaxResult.success(list);
+    }
 
     @GetMapping("/company/dept/list")
-    public AjaxResult companyDeptList() { return AjaxResult.success(new ArrayList<>()); }
+    public AjaxResult companyDeptList(CompanyDept param) {
+        List<CompanyDept> list = companyDeptService != null ?
+            companyDeptService.selectCompanyDeptList(param) : new ArrayList<>();
+        return AjaxResult.success(list);
+    }
 
     @GetMapping("/company/post/list")
-    public TableDataInfo companyPostList() { return getDataTable(new ArrayList<>()); }
+    public TableDataInfo companyPostList(CompanyPost param) {
+        startPage();
+        List<CompanyPost> list = companyPostService != null ?
+            companyPostService.selectCompanyPostList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
 
     @GetMapping("/company/user/list")
-    public TableDataInfo companyUserList() { return getDataTable(new ArrayList<>()); }
+    public TableDataInfo companyUserList(CompanyUser param) {
+        startPage();
+        List<CompanyUser> list = companyUserService != null ?
+            companyUserService.selectCompanyUserList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
 
     @GetMapping("/company/apply/list")
-    public TableDataInfo companyApplyList() { return getDataTable(new ArrayList<>()); }
+    public TableDataInfo companyApplyList(@RequestParam Map<String, Object> param) {
+        startPage();
+        // ICompanyUserChangeApplyService uses Map-based query
+        return getDataTable(new ArrayList<>());
+    }
 
     @PostMapping({"/company/companyPackageOrder/buy", "/company/companySmsPackage/buy",
                   "/company/companyRecharge/doRecharge"})
@@ -339,7 +379,12 @@ public class AdminCompanyBridgeController extends BaseController {
     public AjaxResult companyOrderActionsGet() { return AjaxResult.success(); }
 
     @GetMapping({"/company/companyRecharge/list", "/company/companyRecharge/doRecharge"})
-    public TableDataInfo companyRechargeLists() { return getDataTable(new ArrayList<>()); }
+    public TableDataInfo companyRechargeLists(CompanyRecharge param) {
+        startPage();
+        List<CompanyRecharge> list = companyRechargeService != null ?
+            companyRechargeService.selectCompanyRechargeList(param) : new ArrayList<>();
+        return getDataTable(list);
+    }
 
     // ========== 公司统计 /company/statistics ==========
     @GetMapping({"/company/statistics/voiceLogs", "/company/statistics/myVoiceLogs",

+ 31 - 6
fs-admin-saas/src/main/java/com/fs/admin/controller/AdminStoreMiscController.java

@@ -8,6 +8,7 @@ import com.fs.common.enums.BusinessType;
 import com.fs.hisStore.domain.*;
 import com.fs.hisStore.service.*;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
@@ -20,6 +21,30 @@ import java.util.*;
 @RestController
 public class AdminStoreMiscController extends BaseController {
 
+    @Autowired(required = false)
+    private JdbcTemplate jdbcTemplate;
+
+    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");
+                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;
+    }
+
     @Autowired
     private IFsStoreCouponScrmService storeCouponService;
     @Autowired
@@ -278,31 +303,31 @@ public class AdminStoreMiscController extends BaseController {
 
     @GetMapping("/store/exportTask/list")
     public TableDataInfo storeExportTaskList() {
-        return getDataTable(new ArrayList<>());
+        return safeListFromTable("fs_export_task");
     }
 
     @GetMapping({"/store/healthRecord/list", "/store/healthRecord/myList"})
     public TableDataInfo storeHealthRecordList() {
-        return getDataTable(new ArrayList<>());
+        return safeListFromTable("fs_health_record");
     }
 
     @GetMapping({"/store/inquiryOrder/list", "/store/inquiryOrder/myList"})
     public TableDataInfo storeInquiryOrderList() {
-        return getDataTable(new ArrayList<>());
+        return safeListFromTable("fs_inquiry_order");
     }
 
     @GetMapping({"/store/packageOrder/list", "/store/packageOrder/myList"})
     public TableDataInfo storePackageOrderList() {
-        return getDataTable(new ArrayList<>());
+        return safeListFromTable("fs_package_order");
     }
 
     @GetMapping({"/store/prescribe/myList", "/store/storeAfterSales/myList"})
     public TableDataInfo storeMyLists() {
-        return getDataTable(new ArrayList<>());
+        return safeListFromTable("fs_prescribe");
     }
 
     @GetMapping("/store/inquiryOrderReport/list")
     public TableDataInfo storeInquiryOrderReportList() {
-        return getDataTable(new ArrayList<>());
+        return safeListFromTable("fs_inquiry_order_report");
     }
 }

+ 20 - 0
fs-admin-saas/src/main/java/com/fs/admin/sync/LobsterBridgeDataSyncService.java

@@ -47,6 +47,16 @@ public class LobsterBridgeDataSyncService {
         "workflow_instance"
     };
 
+    /** 需要从租户库同步到 ylrz_saas 镜像的 HIS/store 业务表 */
+    private static final String[] SYNC_TABLES_HIS = {
+        "fs_export_task",
+        "fs_health_record",
+        "fs_inquiry_order",
+        "fs_package_order",
+        "fs_prescribe",
+        "fs_inquiry_order_report"
+    };
+
     @Autowired
     private JdbcTemplate jdbcTemplate;
 
@@ -159,6 +169,16 @@ public class LobsterBridgeDataSyncService {
             }
         }
 
+        for (String table : SYNC_TABLES_HIS) {
+            try {
+                int rows = syncSingleTable(tenant, companyId, table);
+                totalRows += rows;
+            } catch (Exception e) {
+                log.warn("[LobsterSync] 租户 tenantId={}, HIS表 {} 同步失败: {}",
+                    tenant.getId(), table, e.getMessage());
+            }
+        }
+
         if (totalRows > 0) {
             log.debug("[LobsterSync] 租户 tenantId={}, tenantName={} 同步 {} 行",
                 tenant.getId(), tenant.getTenantName(), totalRows);

+ 0 - 14
fs-admin-saas/src/main/java/com/fs/company/controller/workflow/SaasMissingApisStubController.java

@@ -25,7 +25,6 @@ public class SaasMissingApisStubController extends BaseController {
     private static final Set<String> ALLOWED_TABLES = new HashSet<>(Arrays.asList(
         "watch_device_info", "watch_iot_card",
         "shop_msg", "shop_records", "shop_role",
-        "proxy_balance", "proxy_info", "proxy", "proxy_module_usage",
         "fs_promotion_order", "fs_store_adv", "fs_health_store_order",
         "fs_home_article", "fs_home_category", "fs_home_view", "fs_menu",
         "fs_prescribe_drug", "fs_recommend",
@@ -93,19 +92,6 @@ public class SaasMissingApisStubController extends BaseController {
     @GetMapping("/shop/role/list")
     public TableDataInfo shopRole() { return safeListFromTable("shop_role"); }
 
-    // ========== 代理 proxy 核心 ==========
-    @GetMapping("/proxy/balance/list")
-    public TableDataInfo proxyBalance() { return safeListFromTable("proxy_balance"); }
-
-    @GetMapping("/proxy/info/list")
-    public TableDataInfo proxyInfo() { return safeListFromTable("proxy_info"); }
-
-    @GetMapping("/proxy/dashboard/list")
-    public TableDataInfo proxyDashboard() { return safeListFromTable("proxy"); }
-
-    @GetMapping("/proxy/moduleUsage/list")
-    public TableDataInfo proxyModuleUsage() { return safeListFromTable("proxy_module_usage"); }
-
     // ========== 商城 store/* 各子模块 ==========
     @GetMapping("/store/PromotionOrder/list")
     public TableDataInfo storePromotionOrder() { return safeListFromTable("fs_promotion_order"); }

+ 74 - 0
fs-admin-saas/src/main/resources/db/migration/tenant/V20260521_01__add_lobster_engine_menus.sql

@@ -0,0 +1,74 @@
+-- =====================================================
+-- 龙虾引擎 (Lobster Workflow Engine) 菜单初始化
+-- 将前端 /lobster/* 静态路由的12个子页面录入 sys_menu
+-- 适用场景:
+--   1. 新租户创建时从 tenant_sys_menu 模板复制(需同步更新模板表)
+--   2. 现有租户通过 TenantUpgradeService 自动应用
+-- =====================================================
+
+-- 1. 根菜单:龙虾引擎 (parent_id=0 作为顶层菜单)
+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 (29900, '龙虾引擎', 0, 100, '/lobster', '', 'M', 'el-icon-cpu', '0', '0', 0, 0, 'admin', NOW(), 'Lobster Workflow Engine顶层菜单');
+
+-- 2. AI生产工作流 目录(包含画布和模板库两个子页)
+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 (29901, 'AI生产工作流', 29900, 1, 'production-workflow', '', 'M', 'el-icon-component', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 2a. 工作流画布
+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 (29902, '工作流画布', 29901, 1, 'canvas', 'lobster/workflow-canvas/index', 'C', 'el-icon-chart', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 2b. 工作流模板库
+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 (29903, '工作流模板库', 29901, 2, 'template', 'lobster/template/index', 'C', 'el-icon-documentation', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 3. AI生成工作流
+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 (29904, 'AI生成工作流', 29900, 2, 'workflow-generate', 'lobster/workflow-generate/index', 'C', 'el-icon-build', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 4. 实例监控
+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 (29905, '实例监控', 29900, 3, 'instance', 'lobster/instance/index', 'C', 'el-icon-monitor', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 5. AI优化建议
+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 (29906, 'AI优化建议', 29900, 4, 'optimization', 'lobster/optimization/index', 'C', 'el-icon-eye-open', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 6. 提示词管理
+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 (29907, '提示词管理', 29900, 5, 'prompt', 'lobster/prompt/index', 'C', 'el-icon-edit', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 7. 销冠语料学习
+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 (29908, '销冠语料学习', 29900, 6, 'sales-corpus', 'lobster/sales-corpus/index', 'C', 'el-icon-star', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 8. 接口注册中心
+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 (29909, '接口注册中心', 29900, 7, 'api-registry', 'lobster/api-registry/index', 'C', 'el-icon-nested', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 9. 死信队列
+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 (29910, '死信队列', 29900, 8, 'dead-letter', 'lobster/dead-letter/index', 'C', 'el-icon-bug', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 10. 节点审核
+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 (29911, '节点审核', 29900, 9, 'event-audit', 'lobster/event-audit/index', 'C', 'el-icon-checkbox', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 11. 聚合聊天
+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 (29912, '聚合聊天', 29900, 10, 'chat-aggregate', 'lobster/chat-aggregate/index', 'C', 'el-icon-message', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 12. 模型配置
+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 (29913, '模型配置', 29900, 11, 'model-config', 'lobster/model-config/index', 'C', 'el-icon-server', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 13. Token系数管理
+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 (29914, 'Token系数管理', 29900, 12, 'billing', 'lobster/billing/index', 'C', 'el-icon-money', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- =====================================================
+-- 同时更新模板表 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 29900 AND 29914;

+ 28 - 0
fs-admin-saas/src/main/resources/db/migration/tenant/V20260521_02__remove_adminminui_unused_menus.sql

@@ -0,0 +1,28 @@
+-- =====================================================
+-- 从租户 sys_menu 中删除不应出现在 adminminui 的菜单
+-- =====================================================
+-- 删除清单(共 7 个):
+--   类别 A:空占位页面(仅显示"请到总后台管理",无实际功能)
+--     1. admin/dailyStatistics/index  — 每日统计
+--     2. admin/proxy/feeConfig/index  — 代理收费配置
+--     3. admin/serviceCost/index      — 服务成本价配置
+--   类别 B:代理管理页面(归属 adminUI,adminminui 中不应出现)
+--     4. proxy/servicePrice/index     — 服务价格
+--     5. proxy/tenant/index           — 代理租户
+--     6. proxy/tenantRel/index        — 代理租户关系
+--     7. proxy/withdraw/index         — 提现管理
+-- =====================================================
+-- 通过 TenantUpgradeService 自动应用于所有已有租户
+-- =====================================================
+
+DELETE FROM sys_menu WHERE component IN (
+    -- 类别 A:空占位页面
+    'admin/dailyStatistics/index',
+    'admin/proxy/feeConfig/index',
+    'admin/serviceCost/index',
+    -- 类别 B:代理管理页面(归属 adminUI)
+    'proxy/servicePrice/index',
+    'proxy/tenant/index',
+    'proxy/tenantRel/index',
+    'proxy/withdraw/index'
+);

+ 135 - 0
fs-admin/src/main/java/com/fs/admin/controller/AiModelAdminController.java

@@ -0,0 +1,135 @@
+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.enums.BusinessType;
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.service.ai.AdminAiModelService;
+import com.fs.company.service.ai.AiModelGateway;
+import com.fs.company.service.ai.AiProviderManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Admin AI模型配置管理
+ * <p>
+ * 统一管理所有文本AI模型(替代旧的AiProvider/TextModel管理页面)
+ */
+@RestController
+@RequestMapping("/admin/aiModel")
+public class AiModelAdminController extends BaseController {
+
+    @Autowired
+    private AdminAiModelService modelService;
+
+    @Autowired
+    private AiModelGateway aiModelGateway;
+
+    /** 模型列表 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:list')")
+    @GetMapping("/list")
+    public AjaxResult list() {
+        List<AdminAiModel> list = modelService.listAll();
+        list.forEach(this::maskApiKey);
+        return AjaxResult.success(list);
+    }
+
+    /** 模型详情 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:list')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        AdminAiModel model = modelService.getById(id);
+        if (model != null) {
+            maskApiKey(model);
+        }
+        return AjaxResult.success(model);
+    }
+
+    /** 新增模型 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:add')")
+    @Log(title = "新增AI模型", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody AdminAiModel model) {
+        if (model.getStatus() == null) model.setStatus(1);
+        if (model.getSortOrder() == null) model.setSortOrder(0);
+        if (model.getMaxTokens() == null) model.setMaxTokens(4096);
+        if (model.getTemperature() == null) model.setTemperature(0.7);
+        modelService.insert(model);
+        return AjaxResult.success();
+    }
+
+    /** 编辑模型 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:edit')")
+    @Log(title = "修改AI模型", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}")
+    public AjaxResult edit(@PathVariable Long id, @RequestBody AdminAiModel model) {
+        model.setId(id);
+        // 脱敏的apiKey不覆盖原值
+        if (model.getApiKey() != null && model.getApiKey().contains("*")) {
+            AdminAiModel existing = modelService.getById(id);
+            if (existing != null) {
+                model.setApiKey(existing.getApiKey());
+            }
+        }
+        modelService.update(model);
+        return AjaxResult.success();
+    }
+
+    /** 删除模型 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:remove')")
+    @Log(title = "删除AI模型", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{id}")
+    public AjaxResult remove(@PathVariable Long id) {
+        modelService.delete(id);
+        return AjaxResult.success();
+    }
+
+    /** 批量更新排序 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:edit')")
+    @Log(title = "更新AI模型排序", businessType = BusinessType.UPDATE)
+    @PutMapping("/batchSort")
+    public AjaxResult batchSort(@RequestBody List<Map<String, Object>> sortList) {
+        for (Map<String, Object> item : sortList) {
+            Long id = Long.valueOf(item.get("id").toString());
+            Integer sortOrder = Integer.valueOf(item.get("sortOrder").toString());
+            modelService.updateSortOrder(id, sortOrder);
+        }
+        return AjaxResult.success();
+    }
+
+    /** 测试模型连接 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:list')")
+    @PostMapping("/test/{id}")
+    public AjaxResult testConnection(@PathVariable Long id) {
+        AdminAiModel model = modelService.getById(id);
+        if (model == null) {
+            return AjaxResult.error("模型不存在");
+        }
+        AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
+        Map<String, Object> result = aiModelGateway.testConnectionWithConfig(cfg);
+        return AjaxResult.success(result);
+    }
+
+    /** 刷新缓存 */
+    @PreAuthorize("@ss.hasPermi('admin:aiModel:edit')")
+    @PostMapping("/refresh")
+    public AjaxResult refresh() {
+        modelService.refresh();
+        return AjaxResult.success("缓存已刷新");
+    }
+
+    /** API Key脱敏 */
+    private void maskApiKey(AdminAiModel model) {
+        String apiKey = model.getApiKey();
+        if (apiKey != null && apiKey.length() > 8) {
+            model.setApiKey(apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length() - 4));
+        } else if (apiKey != null && apiKey.length() > 0) {
+            model.setApiKey("****");
+        }
+    }
+}

+ 128 - 0
fs-admin/src/main/java/com/fs/admin/controller/AiSceneAdminController.java

@@ -0,0 +1,128 @@
+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.enums.BusinessType;
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.domain.AdminAiScene;
+import com.fs.company.domain.AdminAiSceneModel;
+import com.fs.company.service.ai.AdminAiSceneService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Admin AI场景配置管理
+ * <p>
+ * 管理模型使用场景及场景-模型关联关系,
+ * 支持流水线排序、角色配置和质量阈值设置。
+ */
+@RestController
+@RequestMapping("/admin/aiScene")
+public class AiSceneAdminController extends BaseController {
+
+    @Autowired
+    private AdminAiSceneService sceneService;
+
+    /** 场景列表 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:list')")
+    @GetMapping("/list")
+    public AjaxResult list() {
+        List<AdminAiScene> list = sceneService.listScenes();
+        return AjaxResult.success(list);
+    }
+
+    /** 场景详情 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:list')")
+    @GetMapping("/{sceneCode}")
+    public AjaxResult getInfo(@PathVariable String sceneCode) {
+        AdminAiScene scene = sceneService.getScene(sceneCode);
+        return AjaxResult.success(scene);
+    }
+
+    /** 更新场景配置 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @Log(title = "修改AI场景配置", businessType = BusinessType.UPDATE)
+    @PutMapping("/{sceneCode}")
+    public AjaxResult edit(@PathVariable String sceneCode, @RequestBody AdminAiScene scene) {
+        scene.setSceneCode(sceneCode);
+        sceneService.updateScene(scene);
+        return AjaxResult.success();
+    }
+
+    /** 更新场景质量阈值 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @Log(title = "修改AI场景质量阈值", businessType = BusinessType.UPDATE)
+    @PutMapping("/{sceneCode}/threshold")
+    public AjaxResult updateThreshold(@PathVariable String sceneCode, @RequestBody Map<String, Object> body) {
+        Integer threshold = Integer.valueOf(body.get("qualityThreshold").toString());
+        sceneService.updateThreshold(sceneCode, threshold);
+        return AjaxResult.success();
+    }
+
+    /** 获取场景关联的模型列表(含模型详情) */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:list')")
+    @GetMapping("/{sceneCode}/models")
+    public AjaxResult getSceneModels(@PathVariable String sceneCode) {
+        List<AdminAiSceneModel> list = sceneService.getSceneModels(sceneCode);
+        return AjaxResult.success(list);
+    }
+
+    /** 获取场景下启用的模型列表 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:list')")
+    @GetMapping("/{sceneCode}/enabledModels")
+    public AjaxResult getEnabledModels(@PathVariable String sceneCode) {
+        List<AdminAiModel> list = sceneService.getEnabledModels(sceneCode);
+        return AjaxResult.success(list);
+    }
+
+    /** 场景新增模型关联 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @Log(title = "场景新增模型关联", businessType = BusinessType.INSERT)
+    @PostMapping("/{sceneCode}/models")
+    public AjaxResult addSceneModel(@PathVariable String sceneCode, @RequestBody AdminAiSceneModel rel) {
+        sceneService.addSceneModel(sceneCode, rel.getModelId(),
+                rel.getPipelineOrder(), rel.getRole(), rel.getSortWeight());
+        return AjaxResult.success();
+    }
+
+    /** 删除场景-模型关联 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @Log(title = "删除场景模型关联", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{sceneCode}/models/{id}")
+    public AjaxResult removeSceneModel(@PathVariable String sceneCode, @PathVariable Long id) {
+        sceneService.removeSceneModel(id);
+        return AjaxResult.success();
+    }
+
+    /** 清空场景下所有模型关联 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @Log(title = "清空场景模型关联", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{sceneCode}/models")
+    public AjaxResult clearSceneModels(@PathVariable String sceneCode) {
+        sceneService.clearSceneModels(sceneCode);
+        return AjaxResult.success();
+    }
+
+    /** 更新场景-模型关联排序和角色 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @Log(title = "更新场景模型排序", businessType = BusinessType.UPDATE)
+    @PutMapping("/{sceneCode}/models/{id}")
+    public AjaxResult updateSceneModel(@PathVariable String sceneCode, @PathVariable Long id,
+                                        @RequestBody AdminAiSceneModel rel) {
+        sceneService.updateSceneModelOrder(id, rel.getPipelineOrder(), rel.getRole(), rel.getSortWeight());
+        return AjaxResult.success();
+    }
+
+    /** 刷新缓存 */
+    @PreAuthorize("@ss.hasPermi('admin:aiScene:edit')")
+    @PostMapping("/refresh")
+    public AjaxResult refresh() {
+        sceneService.refresh();
+        return AjaxResult.success("缓存已刷新");
+    }
+}

+ 49 - 0
fs-service/src/main/java/com/fs/company/domain/AdminAiModel.java

@@ -0,0 +1,49 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI模型配置实体(Admin DB统一管理)
+ */
+@Data
+public class AdminAiModel {
+
+    private Long id;
+
+    /** 模型显示名称 */
+    private String modelName;
+
+    /** 供应商编码: doubao/qwen/yuanbao/deepseek */
+    private String providerCode;
+
+    /** 模型标识符, 如 doubao-1-5-pro-32k */
+    private String modelIdentifier;
+
+    /** API地址 */
+    private String apiEndpoint;
+
+    /** API密钥 */
+    private String apiKey;
+
+    /** 嵌入端点(可选) */
+    private String embeddingEndpoint;
+
+    /** 最大Token数 */
+    private Integer maxTokens;
+
+    /** 温度参数 */
+    private Double temperature;
+
+    /** 全局排序,越小越优先 */
+    private Integer sortOrder;
+
+    /** 状态: 1启用 0禁用 */
+    private Integer status;
+
+    private String createBy;
+    private LocalDateTime createTime;
+    private String updateBy;
+    private LocalDateTime updateTime;
+}

+ 37 - 0
fs-service/src/main/java/com/fs/company/domain/AdminAiScene.java

@@ -0,0 +1,37 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * AI模型使用场景定义实体
+ */
+@Data
+public class AdminAiScene {
+
+    private Long id;
+
+    /** 场景编码(唯一) */
+    private String sceneCode;
+
+    /** 场景名称 */
+    private String sceneName;
+
+    /** 场景类型: single单模型 / multi_pipeline多模型流水线 */
+    private String sceneType;
+
+    /** 流水线类型: sequential顺序调用 / scoring质量评分链 */
+    private String pipelineType;
+
+    /** 质量评分通过阈值(满分160, 默认120) */
+    private Integer qualityThreshold;
+
+    /** 状态: 1启用 0禁用 */
+    private Integer status;
+
+    private String createBy;
+    private LocalDateTime createTime;
+    private String updateBy;
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,36 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 场景-模型关联实体(支持排序)
+ */
+@Data
+public class AdminAiSceneModel {
+
+    private Long id;
+
+    /** 关联场景编码 */
+    private String sceneCode;
+
+    /** 关联模型ID */
+    private Long modelId;
+
+    /** 流水线中的顺序(min→max) */
+    private Integer pipelineOrder;
+
+    /** 角色: generator生成者 / scorer评分者 */
+    private String role;
+
+    /** 当多模型同级时的优先级权重 */
+    private Integer sortWeight;
+
+    private LocalDateTime createTime;
+
+    // ─── 关联查询字段(非DB映射) ───
+
+    /** 关联的模型完整信息(JOIN查询时填充) */
+    private AdminAiModel model;
+}

+ 30 - 0
fs-service/src/main/java/com/fs/company/mapper/AdminAiModelMapper.java

@@ -0,0 +1,30 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.AdminAiModel;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AdminAiModel Mapper
+ */
+@Mapper
+public interface AdminAiModelMapper {
+
+    List<AdminAiModel> selectList();
+
+    AdminAiModel selectById(@Param("id") Long id);
+
+    List<AdminAiModel> selectEnabled();
+
+    List<AdminAiModel> selectBySceneCode(@Param("sceneCode") String sceneCode);
+
+    int insert(AdminAiModel model);
+
+    int updateById(AdminAiModel model);
+
+    int deleteById(@Param("id") Long id);
+
+    int updateSortOrder(@Param("id") Long id, @Param("sortOrder") Integer sortOrder);
+}

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

@@ -0,0 +1,26 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.AdminAiScene;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AdminAiScene Mapper
+ */
+@Mapper
+public interface AdminAiSceneMapper {
+
+    List<AdminAiScene> selectList();
+
+    AdminAiScene selectById(@Param("id") Long id);
+
+    AdminAiScene selectByCode(@Param("sceneCode") String sceneCode);
+
+    int insert(AdminAiScene scene);
+
+    int updateById(AdminAiScene scene);
+
+    int updateThreshold(@Param("sceneCode") String sceneCode, @Param("threshold") Integer threshold);
+}

+ 39 - 0
fs-service/src/main/java/com/fs/company/mapper/AdminAiSceneModelMapper.java

@@ -0,0 +1,39 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.AdminAiSceneModel;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * AdminAiSceneModel Mapper(场景-模型关联)
+ */
+@Mapper
+public interface AdminAiSceneModelMapper {
+
+    /** 按场景编码查询关联的模型列表(JOIN admin_ai_model),按pipeline_order排序 */
+    List<AdminAiSceneModel> selectBySceneCode(@Param("sceneCode") String sceneCode);
+
+    /** 按场景编码查询已启用的关联模型(JOIN且status=1) */
+    List<AdminAiSceneModel> selectEnabledBySceneCode(@Param("sceneCode") String sceneCode);
+
+    /** 查询所有关联记录 */
+    List<AdminAiSceneModel> selectAll();
+
+    /** 插入关联 */
+    int insert(AdminAiSceneModel rel);
+
+    /** 按ID删除 */
+    int deleteById(@Param("id") Long id);
+
+    /** 按场景编码删除所有关联 */
+    int deleteBySceneCode(@Param("sceneCode") String sceneCode);
+
+    /** 按场景编码和模型ID删除 */
+    int deleteBySceneAndModel(@Param("sceneCode") String sceneCode, @Param("modelId") Long modelId);
+
+    /** 批量更新pipeline_order */
+    int updateOrder(@Param("id") Long id, @Param("pipelineOrder") Integer pipelineOrder,
+                    @Param("role") String role, @Param("sortWeight") Integer sortWeight);
+}

+ 129 - 0
fs-service/src/main/java/com/fs/company/service/ai/AdminAiModelService.java

@@ -0,0 +1,129 @@
+package com.fs.company.service.ai;
+
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.mapper.AdminAiModelMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * AdminAI模型配置服务(Admin DB统一管理)
+ * <p>
+ * 替代原 AiProviderManager 的模型配置读取部分,
+ * 统一从 admin_ai_model 表获取模型信息。
+ */
+@Service
+public class AdminAiModelService {
+
+    private static final Logger log = LoggerFactory.getLogger(AdminAiModelService.class);
+
+    @Autowired
+    private AdminAiModelMapper modelMapper;
+
+    /** 本地缓存:ID → AdminAiModel */
+    private final Map<Long, AdminAiModel> cache = new ConcurrentHashMap<>();
+
+    /** 按 sort_order 排序的启用模型列表缓存 */
+    private volatile List<AdminAiModel> enabledListCache = Collections.emptyList();
+
+    @PostConstruct
+    public void init() {
+        refresh();
+    }
+
+    /** 刷新本地缓存 */
+    public synchronized void refresh() {
+        try {
+            List<AdminAiModel> all = modelMapper.selectList();
+            cache.clear();
+            List<AdminAiModel> enabled = new ArrayList<>();
+            for (AdminAiModel m : all) {
+                cache.put(m.getId(), m);
+                if (m.getStatus() != null && m.getStatus() == 1) {
+                    enabled.add(m);
+                }
+            }
+            enabled.sort(Comparator.comparing(AdminAiModel::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())));
+            this.enabledListCache = enabled;
+            log.info("[AdminAiModelService] 已加载 {} 个AI模型(启用 {} 个)", all.size(), enabled.size());
+        } catch (Exception e) {
+            log.warn("[AdminAiModelService] 加载模型配置失败: {}", e.getMessage());
+        }
+    }
+
+    /** 获取所有模型(含禁用) */
+    public List<AdminAiModel> listAll() {
+        return new ArrayList<>(cache.values());
+    }
+
+    /** 获取所有启用的模型(按sort_order升序) */
+    public List<AdminAiModel> listEnabled() {
+        return new ArrayList<>(enabledListCache);
+    }
+
+    /** 按ID获取模型 */
+    public AdminAiModel getById(Long id) {
+        AdminAiModel m = cache.get(id);
+        if (m == null) {
+            m = modelMapper.selectById(id);
+            if (m != null) cache.put(id, m);
+        }
+        return m;
+    }
+
+    /** 获取场景下按流水线顺序排列的模型列表 */
+    public List<AdminAiModel> getBySceneCode(String sceneCode) {
+        return modelMapper.selectBySceneCode(sceneCode);
+    }
+
+    /** 新增模型 */
+    public void insert(AdminAiModel model) {
+        if (model.getCreateTime() == null) model.setCreateTime(java.time.LocalDateTime.now());
+        if (model.getUpdateTime() == null) model.setUpdateTime(java.time.LocalDateTime.now());
+        if (model.getCreateBy() == null) model.setCreateBy("system");
+        if (model.getUpdateBy() == null) model.setUpdateBy("system");
+        modelMapper.insert(model);
+        refresh();
+    }
+
+    /** 更新模型 */
+    public void update(AdminAiModel model) {
+        model.setUpdateTime(java.time.LocalDateTime.now());
+        modelMapper.updateById(model);
+        refresh();
+    }
+
+    /** 删除模型 */
+    public void delete(Long id) {
+        modelMapper.deleteById(id);
+        refresh();
+    }
+
+    /** 更新排序 */
+    public void updateSortOrder(Long id, Integer sortOrder) {
+        modelMapper.updateSortOrder(id, sortOrder);
+        refresh();
+    }
+
+    /**
+     * 构建 AiModelGateway 所需的 ProviderConfig(兼容旧接口)
+     */
+    public AiProviderManager.ProviderConfig toProviderConfig(AdminAiModel model) {
+        if (model == null) return null;
+        return new AiProviderManager.ProviderConfig(
+            model.getProviderCode(),
+            model.getModelName(),
+            model.getApiEndpoint(),
+            model.getApiKey(),
+            model.getModelIdentifier(),
+            model.getMaxTokens() != null ? model.getMaxTokens() : 4096,
+            model.getTemperature() != null ? model.getTemperature() : 0.7
+        );
+    }
+}

+ 156 - 0
fs-service/src/main/java/com/fs/company/service/ai/AdminAiSceneService.java

@@ -0,0 +1,156 @@
+package com.fs.company.service.ai;
+
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.domain.AdminAiScene;
+import com.fs.company.domain.AdminAiSceneModel;
+import com.fs.company.mapper.AdminAiSceneMapper;
+import com.fs.company.mapper.AdminAiSceneModelMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * AI模型使用场景服务
+ * <p>
+ * 负责场景定义和场景-模型关联的管理,
+ * 为 AiSceneDispatcher 提供场景配置数据。
+ */
+@Service
+public class AdminAiSceneService {
+
+    private static final Logger log = LoggerFactory.getLogger(AdminAiSceneService.class);
+
+    @Autowired
+    private AdminAiSceneMapper sceneMapper;
+
+    @Autowired
+    private AdminAiSceneModelMapper sceneModelMapper;
+
+    /** 场景缓存:sceneCode → AdminAiScene */
+    private final Map<String, AdminAiScene> sceneCache = new ConcurrentHashMap<>();
+
+    /** 场景-模型关联缓存:sceneCode → 排序后的关联列表 */
+    private final Map<String, List<AdminAiSceneModel>> sceneModelCache = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void init() {
+        refresh();
+    }
+
+    /** 刷新全部缓存 */
+    public synchronized void refresh() {
+        try {
+            List<AdminAiScene> scenes = sceneMapper.selectList();
+            sceneCache.clear();
+            for (AdminAiScene s : scenes) {
+                sceneCache.put(s.getSceneCode(), s);
+            }
+
+            List<AdminAiSceneModel> allModels = sceneModelMapper.selectAll();
+            sceneModelCache.clear();
+            Map<String, List<AdminAiSceneModel>> grouped = allModels.stream()
+                .collect(Collectors.groupingBy(AdminAiSceneModel::getSceneCode));
+            for (Map.Entry<String, List<AdminAiSceneModel>> e : grouped.entrySet()) {
+                e.getValue().sort(Comparator
+                    .comparing(AdminAiSceneModel::getPipelineOrder, Comparator.nullsLast(Comparator.naturalOrder()))
+                    .thenComparing(AdminAiSceneModel::getSortWeight, Comparator.nullsLast(Comparator.naturalOrder())));
+                sceneModelCache.put(e.getKey(), e.getValue());
+            }
+            log.info("[AdminAiSceneService] 已加载 {} 个场景配置", sceneCache.size());
+        } catch (Exception e) {
+            log.warn("[AdminAiSceneService] 刷新场景配置失败: {}", e.getMessage());
+        }
+    }
+
+    /** 获取场景定义 */
+    public AdminAiScene getScene(String sceneCode) {
+        AdminAiScene s = sceneCache.get(sceneCode);
+        if (s == null) {
+            s = sceneMapper.selectByCode(sceneCode);
+            if (s != null) sceneCache.put(sceneCode, s);
+        }
+        return s;
+    }
+
+    /** 获取场景下所有关联模型(含禁用) */
+    public List<AdminAiSceneModel> getSceneModels(String sceneCode) {
+        List<AdminAiSceneModel> cached = sceneModelCache.get(sceneCode);
+        if (cached != null) return cached;
+        List<AdminAiSceneModel> list = sceneModelMapper.selectBySceneCode(sceneCode);
+        sceneModelCache.put(sceneCode, list);
+        return list;
+    }
+
+    /** 获取场景下启用的关联模型(JOIN admin_ai_model where status=1) */
+    public List<AdminAiSceneModel> getEnabledSceneModels(String sceneCode) {
+        return sceneModelMapper.selectEnabledBySceneCode(sceneCode);
+    }
+
+    /** 获取场景下启用的模型列表(直接返回 AdminAiModel) */
+    public List<AdminAiModel> getEnabledModels(String sceneCode) {
+        return getEnabledSceneModels(sceneCode).stream()
+            .map(AdminAiSceneModel::getModel)
+            .filter(Objects::nonNull)
+            .collect(Collectors.toList());
+    }
+
+    /** 列出所有场景 */
+    public List<AdminAiScene> listScenes() {
+        return new ArrayList<>(sceneCache.values());
+    }
+
+    /** 列出所有场景-模型关联 */
+    public List<AdminAiSceneModel> listAllSceneModels() {
+        return sceneModelMapper.selectAll();
+    }
+
+    /** 更新场景 */
+    public void updateScene(AdminAiScene scene) {
+        scene.setUpdateTime(LocalDateTime.now());
+        sceneMapper.updateById(scene);
+        refresh();
+    }
+
+    /** 更新场景质量阈值 */
+    public void updateThreshold(String sceneCode, Integer threshold) {
+        sceneMapper.updateThreshold(sceneCode, threshold);
+        refresh();
+    }
+
+    /** 新增场景-模型关联 */
+    public void addSceneModel(String sceneCode, Long modelId, Integer pipelineOrder, String role, Integer sortWeight) {
+        AdminAiSceneModel rel = new AdminAiSceneModel();
+        rel.setSceneCode(sceneCode);
+        rel.setModelId(modelId);
+        rel.setPipelineOrder(pipelineOrder != null ? pipelineOrder : 0);
+        rel.setRole(role != null ? role : "generator");
+        rel.setSortWeight(sortWeight != null ? sortWeight : 0);
+        sceneModelMapper.insert(rel);
+        refresh();
+    }
+
+    /** 删除场景-模型关联 */
+    public void removeSceneModel(Long id) {
+        sceneModelMapper.deleteById(id);
+        refresh();
+    }
+
+    /** 清空场景下所有模型关联 */
+    public void clearSceneModels(String sceneCode) {
+        sceneModelMapper.deleteBySceneCode(sceneCode);
+        refresh();
+    }
+
+    /** 更新场景-模型关联的排序/角色 */
+    public void updateSceneModelOrder(Long id, Integer pipelineOrder, String role, Integer sortWeight) {
+        sceneModelMapper.updateOrder(id, pipelineOrder, role, sortWeight);
+        refresh();
+    }
+}

+ 28 - 0
fs-service/src/main/java/com/fs/company/service/ai/AiModelGateway.java

@@ -118,6 +118,34 @@ public class AiModelGateway {
         return result;
     }
 
+    /**
+     * 使用 ProviderConfig 直接调用(流水线引擎入口)
+     * 绕过 providerCode 查找,直接使用完整的模型配置
+     */
+    public ModelResponse chatWithConfig(String prompt, String systemPrompt, AiProviderManager.ProviderConfig cfg) {
+        if (cfg == null) {
+            cfg = providerManager.getDefaultConfig();
+        }
+        return invokeOpenAiCompatible(prompt, systemPrompt, cfg);
+    }
+
+    /**
+     * 测试连接(使用 ProviderConfig)
+     */
+    public Map<String, Object> testConnectionWithConfig(AiProviderManager.ProviderConfig cfg) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            ModelResponse resp = invokeOpenAiCompatible("你好,请回复OK", null, cfg);
+            result.put("success", resp != null && !resp.getContent().isEmpty());
+            result.put("response", resp != null ? resp.getContent().substring(0, Math.min(200, resp.getContent().length())) : "");
+            result.put("tokens", resp != null ? resp.getPromptTokens() : 0);
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("error", e.getMessage());
+        }
+        return result;
+    }
+
     // ─── 核心 HTTP ───
 
     private ModelResponse invokeOpenAiCompatible(String prompt, String systemPrompt,

+ 68 - 97
fs-service/src/main/java/com/fs/company/service/ai/AiProviderManager.java

@@ -12,13 +12,11 @@ import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * 大模型供应商管理器(DB驱动,去硬编码
+ * 大模型供应商管理器(改造版:从Admin DB读取
  * <p>
- * 支持4种国产大模型:豆包(Doubao)、通义千问(Qwen)、元宝/混元(Yuanbao)、DeepSeek
- * 全部使用 OpenAI 兼容协议,无需区分 HTTP 调用。
- * <p>
- * 配置来源:company_ai_provider 表(总后台大模型管理页面维护)
- * 降级策略:DB 无配置时使用内置预设(endpoint 固定,需配 API Key)
+ * 改造后从 AdminAiModelService 获取模型配置,
+ * 不再硬编码4种模型预设。ProviderConfig 内部类保留用于兼容旧API。
+ * 新代码应直接使用 AdminAiModelService 和 AiSceneDispatcher。
  */
 @Component
 public class AiProviderManager {
@@ -28,40 +26,18 @@ public class AiProviderManager {
     @Autowired
     private CompanyAiProviderMapper aiProviderMapper;
 
-    /** DB 缓存:providerCode → CompanyAiProvider */
-    private final Map<String, CompanyAiProvider> cache = new ConcurrentHashMap<>();
+    @Autowired(required = false)
+    private AdminAiModelService adminModelService;
 
-    /** 内置4种模型的预设参数 */
-    public static final Map<String, ProviderPreset> PRESETS = new LinkedHashMap<>();
-    static {
-        PRESETS.put("doubao", new ProviderPreset(
-            "豆包(Doubao)", "doubao-pro-4k",
-            "https://ark.cn-beijing.volces.com/api/v3/chat/completions",
-            "https://ark.cn-beijing.volces.com/api/v3/embeddings",
-            4096, 0.7));
-        PRESETS.put("qwen", new ProviderPreset(
-            "通义千问(Qwen)", "qwen-plus",
-            "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
-            "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings",
-            8192, 0.7));
-        PRESETS.put("yuanbao", new ProviderPreset(
-            "元宝/混元(Yuanbao)", "hunyuan-lite",
-            "https://api.hunyuan.cloud.tencent.com/v1/chat/completions",
-            null,
-            4096, 0.7));
-        PRESETS.put("deepseek", new ProviderPreset(
-            "DeepSeek", "deepseek-chat",
-            "https://api.deepseek.com/v1/chat/completions",
-            null,
-            8192, 0.7));
-    }
+    /** 旧DB缓存(兼容过渡期) */
+    private final Map<String, CompanyAiProvider> cache = new ConcurrentHashMap<>();
 
     @PostConstruct
     public void init() {
         refresh();
     }
 
-    /** 刷新缓存 */
+    /** 刷新旧缓存 */
     public synchronized void refresh() {
         try {
             List<CompanyAiProvider> list = aiProviderMapper.selectList();
@@ -72,69 +48,91 @@ public class AiProviderManager {
                     cache.put(p.getProviderCode(), p);
                 }
             }
-            log.info("[AiProviderManager] 已加载 {} 个 AI 供应商", cache.size());
+            log.info("[AiProviderManager] 已加载 {} 个旧AI供应商 (迁移到Admin DB后此缓存将不再使用)", cache.size());
         } catch (Exception e) {
             log.warn("[AiProviderManager] 加载供应商失败: {}", e.getMessage());
         }
     }
 
     /**
-     * 获取指定供应商配置(优先DB,降级预设)
+     * 获取指定供应商配置
+     * 优先 Admin DB (AdminAiModelService),降级旧DB (company_ai_provider)
      */
     public ProviderConfig getConfig(String providerCode) {
-        // 1. DB 中查找
-        CompanyAiProvider db = cache.get(providerCode);
-        if (db != null) {
-            return fromDb(db);
-        }
-        // 2. 尝试查默认供应商
-        for (CompanyAiProvider p : cache.values()) {
-            if (p.getIsDefault() != null && p.getIsDefault() == 1) {
-                return fromDb(p);
+        // 1. 优先从Admin DB读取
+        if (adminModelService != null) {
+            List<com.fs.company.domain.AdminAiModel> enabled = adminModelService.listEnabled();
+            // 查找匹配 providerCode 的模型
+            for (com.fs.company.domain.AdminAiModel m : enabled) {
+                if (providerCode != null && providerCode.equals(m.getProviderCode())) {
+                    return adminModelService.toProviderConfig(m);
+                }
+            }
+            // 如果指定providerCode找不到,返回排序第一的默认模型
+            if (!enabled.isEmpty()) {
+                return adminModelService.toProviderConfig(enabled.get(0));
             }
         }
-        // 3. 预设降级
-        ProviderPreset preset = PRESETS.get(providerCode);
-        if (preset != null) {
-            log.warn("[AiProviderManager] 供应商 {} 无DB配置,使用预设endpoint(需在页面配API Key)", providerCode);
-            return fromPreset(providerCode, preset);
+        // 2. 降级:旧 company_ai_provider 表
+        CompanyAiProvider db = cache.get(providerCode);
+        if (db != null) return fromDb(db);
+        for (CompanyAiProvider p : cache.values()) {
+            if (p.getIsDefault() != null && p.getIsDefault() == 1) return fromDb(p);
         }
-        // 4. 最后兜底:豆包预设
-        ProviderPreset fallback = PRESETS.get("doubao");
-        log.warn("[AiProviderManager] 无可用供应商,降级为豆包预设");
-        return fromPreset("doubao", fallback);
+        // 3. 最终兜底
+        log.warn("[AiProviderManager] 无可用供应商: {}", providerCode);
+        return new ProviderConfig("fallback", "Fallback", "", "", "default", 4096, 0.7);
     }
 
     /** 获取默认供应商配置 */
     public ProviderConfig getDefaultConfig() {
+        if (adminModelService != null) {
+            List<com.fs.company.domain.AdminAiModel> enabled = adminModelService.listEnabled();
+            if (!enabled.isEmpty()) {
+                return adminModelService.toProviderConfig(enabled.get(0));
+            }
+        }
         for (CompanyAiProvider p : cache.values()) {
             if (p.getIsDefault() != null && p.getIsDefault() == 1 && p.getEnabled() == 1) {
                 return fromDb(p);
             }
         }
-        return getConfig("doubao");
+        return getConfig(null);
     }
 
-    /** 按 providerCode 获取(用于 AI 外呼等需要指定模型场景) */
+    /** 按 providerCode 获取 */
     public ProviderConfig getByCodeOrNull(String providerCode) {
         if (providerCode == null || providerCode.isEmpty()) return getDefaultConfig();
-        CompanyAiProvider db = cache.get(providerCode);
-        return db != null ? fromDb(db) : null;
+        return getConfig(providerCode);
     }
 
     /** 可用供应商列表(前端展示) */
     public List<Map<String, Object>> getAvailableProviders() {
+        if (adminModelService != null) {
+            List<com.fs.company.domain.AdminAiModel> enabled = adminModelService.listEnabled();
+            List<Map<String, Object>> list = new ArrayList<>();
+            for (com.fs.company.domain.AdminAiModel m : enabled) {
+                Map<String, Object> map = new LinkedHashMap<>();
+                map.put("providerCode", m.getProviderCode());
+                map.put("providerName", m.getModelName());
+                map.put("modelName", m.getModelIdentifier());
+                map.put("apiEndpoint", m.getApiEndpoint());
+                map.put("maxTokens", m.getMaxTokens());
+                map.put("temperature", m.getTemperature());
+                map.put("configured", true);
+                list.add(map);
+            }
+            return list;
+        }
+        // 降级到旧方式
         List<Map<String, Object>> list = new ArrayList<>();
-        for (String code : PRESETS.keySet()) {
+        for (CompanyAiProvider p : cache.values()) {
             Map<String, Object> m = new LinkedHashMap<>();
-            ProviderPreset preset = PRESETS.get(code);
-            m.put("providerCode", code);
-            m.put("providerName", preset.displayName);
-            m.put("modelName", preset.modelName);
-            m.put("apiEndpoint", preset.apiEndpoint);
-            m.put("maxTokens", preset.maxTokens);
-            m.put("temperature", preset.temperature);
-            m.put("configured", cache.containsKey(code));
+            m.put("providerCode", p.getProviderCode());
+            m.put("providerName", p.getProviderName());
+            m.put("modelName", p.getModelName());
+            m.put("apiEndpoint", p.getApiEndpoint());
+            m.put("configured", true);
             list.add(m);
         }
         return list;
@@ -143,21 +141,13 @@ public class AiProviderManager {
     // ─── 内部方法 ───
 
     private ProviderConfig fromDb(CompanyAiProvider db) {
-        ProviderPreset preset = PRESETS.get(db.getProviderCode());
         return new ProviderConfig(
             db.getProviderCode(), db.getProviderName(),
-            coalesce(db.getApiEndpoint(), preset != null ? preset.apiEndpoint : ""),
+            coalesce(db.getApiEndpoint(), ""),
             coalesce(db.getApiKey(), ""),
-            coalesce(db.getModelName(), preset != null ? preset.modelName : "default"),
-            coalesce(db.getMaxTokens(), preset != null ? preset.maxTokens : 4096),
-            coalesce(db.getTemperature(), preset != null ? preset.temperature : 0.7)
-        );
-    }
-
-    private ProviderConfig fromPreset(String code, ProviderPreset preset) {
-        return new ProviderConfig(
-            code, preset.displayName, preset.apiEndpoint, "",
-            preset.modelName, preset.maxTokens, preset.temperature
+            coalesce(db.getModelName(), "default"),
+            coalesce(db.getMaxTokens(), 4096),
+            coalesce(db.getTemperature(), 0.7)
         );
     }
 
@@ -168,25 +158,6 @@ public class AiProviderManager {
 
     // ─── 内部类 ───
 
-    public static class ProviderPreset {
-        public final String displayName;
-        public final String modelName;
-        public final String apiEndpoint;
-        public final String embeddingEndpoint;
-        public final int maxTokens;
-        public final double temperature;
-
-        ProviderPreset(String displayName, String modelName, String apiEndpoint,
-                      String embeddingEndpoint, int maxTokens, double temperature) {
-            this.displayName = displayName;
-            this.modelName = modelName;
-            this.apiEndpoint = apiEndpoint;
-            this.embeddingEndpoint = embeddingEndpoint;
-            this.maxTokens = maxTokens;
-            this.temperature = temperature;
-        }
-    }
-
     public static class ProviderConfig {
         public final String providerCode;
         public final String providerName;

+ 203 - 0
fs-service/src/main/java/com/fs/company/service/ai/AiSceneDispatcher.java

@@ -0,0 +1,203 @@
+package com.fs.company.service.ai;
+
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.domain.AdminAiScene;
+import com.fs.company.domain.AdminAiSceneModel;
+import com.fs.company.service.llm.ModelResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * AI模型场景分发器(统一入口)
+ * <p>
+ * 替代 MultiModelRouter 的核心路由角色。
+ * <ul>
+ *   <li>单模型场景(single):按排序取优先级最高的可用模型,直接调用</li>
+ *   <li>多模型流水线(multi_pipeline):委托给 MultiModelPipelineEngine</li>
+ * </ul>
+ */
+@Service
+public class AiSceneDispatcher {
+
+    private static final Logger log = LoggerFactory.getLogger(AiSceneDispatcher.class);
+
+    @Autowired
+    private AdminAiSceneService sceneService;
+
+    @Autowired
+    private AdminAiModelService modelService;
+
+    @Autowired
+    private AiModelGateway aiModelGateway;
+
+    @Autowired
+    private MultiModelPipelineEngine pipelineEngine;
+
+    /**
+     * 场景化模型调用(单模型场景入口)
+     *
+     * @param prompt       用户提示词
+     * @param sceneCode    场景编码
+     * @param systemPrompt 系统提示词(可为null)
+     * @return 模型返回内容,失败返回空字符串
+     */
+    public String dispatch(String prompt, String sceneCode, String systemPrompt) {
+        try {
+            AdminAiModel model = pickBestModel(sceneCode);
+            if (model == null) {
+                log.warn("[AiSceneDispatcher] 场景 {} 无可用模型,尝试降级", sceneCode);
+                model = pickFallbackModel();
+            }
+            if (model == null) {
+                log.error("[AiSceneDispatcher] 无任何可用模型");
+                return "";
+            }
+
+            AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
+            ModelResponse resp = aiModelGateway.chatWithConfig(prompt, systemPrompt, cfg);
+            String content = resp != null ? resp.getContent() : "";
+            log.debug("[AiSceneDispatcher] 场景 {} → 模型 {} | tokens: in={} out={}",
+                    sceneCode, model.getModelName(),
+                    resp != null ? resp.getPromptTokens() : 0,
+                    resp != null ? resp.getCompletionTokens() : 0);
+            return content;
+
+        } catch (Exception e) {
+            log.error("[AiSceneDispatcher] 场景 {} 调用失败: {}", sceneCode, e.getMessage());
+            return "";
+        }
+    }
+
+    /**
+     * 场景化调用(返回详细信息含Token用量)
+     */
+    public ModelResponse dispatchWithTokens(String prompt, String sceneCode, String systemPrompt) {
+        try {
+            AdminAiModel model = pickBestModel(sceneCode);
+            if (model == null) model = pickFallbackModel();
+            if (model == null) return new ModelResponse("", null, null, "none");
+
+            AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
+            return aiModelGateway.chatWithConfig(prompt, systemPrompt, cfg);
+        } catch (Exception e) {
+            log.error("[AiSceneDispatcher] 场景 {} dispatchWithTokens失败: {}", sceneCode, e.getMessage());
+            return new ModelResponse("", null, null, "error");
+        }
+    }
+
+    /**
+     * 多模型流水线入口
+     *
+     * @param sceneCode 多模型场景编码
+     * @param params    场景参数(由调用方按需传入)
+     * @return 场景执行结果容器
+     */
+    public Map<String, Object> executePipeline(String sceneCode, Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        AdminAiScene scene = sceneService.getScene(sceneCode);
+        if (scene == null || !"multi_pipeline".equals(scene.getSceneType())) {
+            result.put("success", false);
+            result.put("error", "场景不存在或非多模型场景: " + sceneCode);
+            return result;
+        }
+
+        List<AdminAiModel> models = sceneService.getEnabledModels(sceneCode);
+        if (models.isEmpty()) {
+            result.put("success", false);
+            result.put("error", "场景 " + sceneCode + " 无可用模型");
+            return result;
+        }
+
+        if ("sequential".equals(scene.getPipelineType())) {
+            // ── 顺序调用流水线 ──
+            @SuppressWarnings("unchecked")
+            List<String> prompts = (List<String>) params.getOrDefault("prompts", Collections.emptyList());
+            @SuppressWarnings("unchecked")
+            List<String> systemPrompts = (List<String>) params.getOrDefault("systemPrompts", Collections.emptyList());
+            String fallback = (String) params.getOrDefault("fallback", "{}");
+
+            MultiModelPipelineEngine.SequentialPipelineResult pipeResult =
+                pipelineEngine.executeSequential(models, prompts, systemPrompts, fallback);
+
+            result.put("success", pipeResult.isSuccess());
+            result.put("finalOutput", pipeResult.getFinalOutput());
+            result.put("stages", pipeResult.getStages());
+
+        } else if ("scoring".equals(scene.getPipelineType())) {
+            // ── 质量评分流水线 ──
+            String generatePrompt = (String) params.get("generatePrompt");
+            MultiModelPipelineEngine.ScoringPromptBuilder scoringBuilder =
+                (MultiModelPipelineEngine.ScoringPromptBuilder) params.get("scoringBuilder");
+            MultiModelPipelineEngine.RegeneratePromptBuilder regenerateBuilder =
+                (MultiModelPipelineEngine.RegeneratePromptBuilder) params.get("regenerateBuilder");
+            int threshold = scene.getQualityThreshold() != null ? scene.getQualityThreshold() : 120;
+            String defaultContent = (String) params.getOrDefault("defaultContent", "");
+
+            MultiModelPipelineEngine.ScoringPipelineResult pipeResult =
+                pipelineEngine.executeScoring(models, generatePrompt, scoringBuilder, regenerateBuilder,
+                    threshold, defaultContent);
+
+            result.put("success", pipeResult.isSuccess());
+            result.put("finalContent", pipeResult.getFinalContent());
+            result.put("finalScore", pipeResult.getFinalScore());
+            result.put("scoringEnabled", pipeResult.isScoringEnabled());
+            result.put("feedback", pipeResult.getFeedback());
+            result.put("stages", pipeResult.getStages());
+        } else {
+            result.put("success", false);
+            result.put("error", "未知流水线类型: " + scene.getPipelineType());
+        }
+
+        return result;
+    }
+
+    // ═══════════════════════════════════════════
+    //  私有方法
+    // ═══════════════════════════════════════════
+
+    /**
+     * 从场景中挑选最佳模型(优先级最高且启用)
+     */
+    private AdminAiModel pickBestModel(String sceneCode) {
+        List<AdminAiModel> models = sceneService.getEnabledModels(sceneCode);
+        if (models.isEmpty()) {
+            // 场景未绑定模型,尝试全局默认
+            List<AdminAiModel> allEnabled = modelService.listEnabled();
+            if (!allEnabled.isEmpty()) {
+                log.info("[AiSceneDispatcher] 场景 {} 未绑定模型,使用全局第一个可用模型", sceneCode);
+                return allEnabled.get(0);
+            }
+            return null;
+        }
+        return models.get(0); // 已按pipeline_order和sort_weight排序,取第一个
+    }
+
+    /**
+     * 降级兜底模型(全局排序第一的启用模型)
+     */
+    private AdminAiModel pickFallbackModel() {
+        List<AdminAiModel> allEnabled = modelService.listEnabled();
+        return allEnabled.isEmpty() ? null : allEnabled.get(0);
+    }
+
+    /**
+     * 获取场景下所有可用模型信息(供前端展示/调试)
+     */
+    public List<Map<String, Object>> getSceneModelInfo(String sceneCode) {
+        List<AdminAiModel> models = sceneService.getEnabledModels(sceneCode);
+        return models.stream().map(m -> {
+            Map<String, Object> info = new LinkedHashMap<>();
+            info.put("id", m.getId());
+            info.put("modelName", m.getModelName());
+            info.put("providerCode", m.getProviderCode());
+            info.put("modelIdentifier", m.getModelIdentifier());
+            info.put("sortOrder", m.getSortOrder());
+            return info;
+        }).collect(Collectors.toList());
+    }
+}

+ 439 - 0
fs-service/src/main/java/com/fs/company/service/ai/MultiModelPipelineEngine.java

@@ -0,0 +1,439 @@
+package com.fs.company.service.ai;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.domain.AdminAiSceneModel;
+import com.fs.company.service.llm.ModelResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+/**
+ * 多模型流水线引擎
+ * <p>
+ * 支持两种流水线模式:
+ * <ul>
+ *   <li><b>sequential(顺序调用)</b>: A→B→C,每个模型接收上一阶段的输出,最终输出+评分</li>
+ *   <li><b>scoring(质量评分链)</b>: 生成→评分→(不通过)重新生成→再次评分→...→末尾降权</li>
+ * </ul>
+ */
+@Service
+public class MultiModelPipelineEngine {
+
+    private static final Logger log = LoggerFactory.getLogger(MultiModelPipelineEngine.class);
+
+    /** 质量评分满分(8维度 × 20分) */
+    public static final int FULL_SCORE = 160;
+
+    @Autowired
+    private AiModelGateway aiModelGateway;
+
+    @Autowired
+    private AdminAiModelService modelService;
+
+    // ═══════════════════════════════════════════
+    //  1. 顺序调用流水线 (Sequential)
+    // ═══════════════════════════════════════════
+
+    /**
+     * 顺序调用流水线:按模型列表顺序依次生成
+     * <p>
+     * model[0] → 初始生成 → model[1] → 改进 → model[2] → 验证 → ...
+     * 每个阶段失败时跳过后续,使用前阶段输出。
+     *
+     * @param models   排序后的关联模型列表
+     * @param prompts  每个阶段的提示词(按索引对应)
+     * @param fallback 全部失败时的兜底值
+     * @return 流水线执行结果
+     */
+    public SequentialPipelineResult executeSequential(List<AdminAiModel> models,
+                                                       List<String> prompts,
+                                                       List<String> systemPrompts,
+                                                       String fallback) {
+        SequentialPipelineResult result = new SequentialPipelineResult();
+        if (models == null || models.isEmpty()) {
+            result.setSuccess(false);
+            result.setFinalOutput(fallback);
+            return result;
+        }
+
+        List<PipelineStageResult> stages = new ArrayList<>();
+        String currentOutput = null;
+
+        for (int i = 0; i < models.size(); i++) {
+            AdminAiModel model = models.get(i);
+            String stagePrompt = (prompts != null && i < prompts.size()) ? prompts.get(i) : (currentOutput + "\n\n请完善以上内容");
+            String stageSystem = (systemPrompts != null && i < systemPrompts.size()) ? systemPrompts.get(i) : null;
+
+            try {
+                AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
+                ModelResponse resp = aiModelGateway.chatWithConfig(stagePrompt, stageSystem, cfg);
+
+                PipelineStageResult stage = new PipelineStageResult();
+                stage.setStageIndex(i);
+                stage.setModelName(model.getModelName());
+                stage.setModelIdentifier(model.getModelIdentifier());
+                stage.setOutput(resp != null ? resp.getContent() : null);
+                stage.setPromptTokens(resp != null ? resp.getPromptTokens() : 0);
+                stage.setCompletionTokens(resp != null ? resp.getCompletionTokens() : 0);
+
+                if (resp != null && resp.getContent() != null && !resp.getContent().isEmpty()) {
+                    currentOutput = resp.getContent();
+                    stage.setSuccess(true);
+                } else {
+                    stage.setSuccess(false);
+                }
+                stages.add(stage);
+
+            } catch (Exception e) {
+                log.warn("[Pipeline] 阶段 {} (模型 {}) 失败: {}", i, model.getModelName(), e.getMessage());
+                PipelineStageResult stage = new PipelineStageResult();
+                stage.setStageIndex(i);
+                stage.setModelName(model.getModelName());
+                stage.setSuccess(false);
+                stage.setError(e.getMessage());
+                stages.add(stage);
+                break; // 失败后跳出,使用上一阶段的输出
+            }
+        }
+
+        result.setStages(stages);
+        result.setFinalOutput(currentOutput != null ? currentOutput : fallback);
+        result.setSuccess(currentOutput != null);
+        return result;
+    }
+
+    // ═══════════════════════════════════════════
+    //  2. 质量评分流水线 (Scoring)
+    // ═══════════════════════════════════════════
+
+    /**
+     * 质量评分流水线执行结果
+     */
+    public static class ScoringPipelineResult {
+        private boolean success;
+        private String finalContent;           // 最终AI回复
+        private int finalScore;                // 最终评分
+        private boolean scoringEnabled;        // 是否启用了评分
+        private List<ScoringStageResult> stages = new ArrayList<>();
+        private String feedback;               // 最后的改进建议
+
+        public boolean isSuccess() { return success; }
+        public void setSuccess(boolean success) { this.success = success; }
+        public String getFinalContent() { return finalContent; }
+        public void setFinalContent(String finalContent) { this.finalContent = finalContent; }
+        public int getFinalScore() { return finalScore; }
+        public void setFinalScore(int finalScore) { this.finalScore = finalScore; }
+        public boolean isScoringEnabled() { return scoringEnabled; }
+        public void setScoringEnabled(boolean scoringEnabled) { this.scoringEnabled = scoringEnabled; }
+        public List<ScoringStageResult> getStages() { return stages; }
+        public String getFeedback() { return feedback; }
+        public void setFeedback(String feedback) { this.feedback = feedback; }
+    }
+
+    public static class ScoringStageResult {
+        private int stageIndex;
+        private String modelName;
+        private String role;       // generator / scorer
+        private String content;    // 生成的内容 或 评分JSON
+        private int score;         // 仅评分阶段有值
+        private boolean passed;    // 评分是否通过
+        private boolean success;
+        private String error;
+
+        public int getStageIndex() { return stageIndex; }
+        public void setStageIndex(int stageIndex) { this.stageIndex = stageIndex; }
+        public String getModelName() { return modelName; }
+        public void setModelName(String modelName) { this.modelName = modelName; }
+        public String getRole() { return role; }
+        public void setRole(String role) { this.role = role; }
+        public String getContent() { return content; }
+        public void setContent(String content) { this.content = content; }
+        public int getScore() { return score; }
+        public void setScore(int score) { this.score = score; }
+        public boolean isPassed() { return passed; }
+        public void setPassed(boolean passed) { this.passed = passed; }
+        public boolean isSuccess() { return success; }
+        public void setSuccess(boolean success) { this.success = success; }
+        public String getError() { return error; }
+        public void setError(String error) { this.error = error; }
+    }
+
+    /**
+     * 执行质量评分流水线
+     * <p>
+     * 规则:
+     * <ul>
+     *   <li>1个模型 → 不启用评分,直接用该模型生成</li>
+     *   <li>≥2个模型 → 启用评分链</li>
+     *   <li>偶数索引(0,2,4...) = generator,奇数索引(1,3,5...) = scorer</li>
+     *   <li>末尾模型 = 降权处理(不自评分,直接生成)</li>
+     *   <li>评分通过阈值 → 结束流水线,返回当前内容</li>
+     * </ul>
+     *
+     * @param models        排序后的关联模型列表
+     * @param generatePrompt 初始生成提示词
+     * @param scoringPromptBuilder 评分提示词构建函数(接收content,返回评分prompt)
+     * @param regeneratePromptBuilder 重新生成提示词构建函数(接收content+feedback,返回再生prompt)
+     * @param threshold     评分通过阈值
+     * @param defaultContent 全部失败时的兜底内容
+     * @return 评分流水线结果
+     */
+    public ScoringPipelineResult executeScoring(List<AdminAiModel> models,
+                                                  String generatePrompt,
+                                                  ScoringPromptBuilder scoringPromptBuilder,
+                                                  RegeneratePromptBuilder regeneratePromptBuilder,
+                                                  int threshold,
+                                                  String defaultContent) {
+        ScoringPipelineResult result = new ScoringPipelineResult();
+
+        if (models == null || models.isEmpty()) {
+            result.setSuccess(false);
+            result.setFinalContent(defaultContent);
+            result.setScoringEnabled(false);
+            return result;
+        }
+
+        int modelCount = models.size();
+        boolean scoringEnabled = modelCount >= 2;
+        result.setScoringEnabled(scoringEnabled);
+
+        // ── 只有1个模型 → 不启用评分,直接生成 ──
+        if (!scoringEnabled) {
+            AdminAiModel model = models.get(0);
+            try {
+                AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
+                ModelResponse resp = aiModelGateway.chatWithConfig(generatePrompt, null, cfg);
+
+                ScoringStageResult stage = new ScoringStageResult();
+                stage.setStageIndex(0);
+                stage.setModelName(model.getModelName());
+                stage.setRole("generator");
+                stage.setContent(resp != null ? resp.getContent() : null);
+                stage.setSuccess(resp != null && resp.getContent() != null);
+                result.getStages().add(stage);
+
+                result.setFinalContent(resp != null ? resp.getContent() : defaultContent);
+                result.setFinalScore(0);
+                result.setSuccess(resp != null && resp.getContent() != null);
+            } catch (Exception e) {
+                log.warn("[ScoringPipeline] 单模型生成失败: {}", e.getMessage());
+                result.setSuccess(false);
+                result.setFinalContent(defaultContent);
+            }
+            return result;
+        }
+
+        // ── ≥2个模型 → 启用评分链 ──
+        String currentContent = null;
+        String feedback = null;
+        boolean passed = false;
+
+        for (int i = 0; i < modelCount; i++) {
+            AdminAiModel model = models.get(i);
+            boolean isLast = (i == modelCount - 1);
+            boolean isGenerator = (i % 2 == 0);  // 偶数=生成者
+            boolean isScorer = !isGenerator;       // 奇数=评分者
+
+            // 如果是最后一个模型,降权处理(只生成,不自评分)
+            if (isLast) {
+                isGenerator = true;
+                isScorer = false;
+            }
+
+            ScoringStageResult stage = new ScoringStageResult();
+            stage.setStageIndex(i);
+            stage.setModelName(model.getModelName());
+
+            try {
+                AiProviderManager.ProviderConfig cfg = modelService.toProviderConfig(model);
+
+                if (isGenerator) {
+                    // ── 生成阶段 ──
+                    stage.setRole("generator");
+                    String prompt;
+                    if (currentContent == null) {
+                        prompt = generatePrompt;
+                    } else {
+                        prompt = regeneratePromptBuilder != null
+                            ? regeneratePromptBuilder.build(currentContent, feedback)
+                            : feedback + "\n\n请根据以上反馈重新生成回复:" + generatePrompt;
+                    }
+                    ModelResponse resp = aiModelGateway.chatWithConfig(prompt, null, cfg);
+                    currentContent = resp != null ? resp.getContent() : currentContent;
+                    stage.setContent(currentContent);
+                    stage.setSuccess(currentContent != null);
+
+                } else {
+                    // ── 评分阶段 ──
+                    stage.setRole("scorer");
+                    if (currentContent == null) {
+                        // 无内容可评分,跳过
+                        stage.setSuccess(false);
+                        stage.setError("无内容可评分");
+                    } else {
+                        String scoringPrompt = scoringPromptBuilder != null
+                            ? scoringPromptBuilder.build(currentContent)
+                            : buildDefaultScoringPrompt(currentContent);
+
+                        ModelResponse resp = aiModelGateway.chatWithConfig(scoringPrompt, null, cfg);
+                        String scoringJson = resp != null ? resp.getContent() : "";
+                        stage.setContent(scoringJson);
+
+                        int score = parseScoreFromResponse(scoringJson);
+                        stage.setScore(score);
+                        passed = score >= threshold;
+                        stage.setPassed(passed);
+                        stage.setSuccess(true);
+
+                        if (passed) {
+                            // 评分通过,结束流水线
+                            feedback = extractFeedback(scoringJson);
+                            break;
+                        } else {
+                            // 评分不通过,提取反馈供下一阶段重新生成
+                            feedback = extractFeedback(scoringJson);
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("[ScoringPipeline] 阶段 {} (模型 {}) 失败: {}", i, model.getModelName(), e.getMessage());
+                stage.setSuccess(false);
+                stage.setError(e.getMessage());
+            }
+
+            result.getStages().add(stage);
+
+            if (passed) break; // 评分通过,结束流水线
+        }
+
+        // ── 最终结果 ──
+        ScoringStageResult lastStage = !result.getStages().isEmpty()
+            ? result.getStages().get(result.getStages().size() - 1) : null;
+
+        result.setFinalContent(currentContent != null ? currentContent : defaultContent);
+        result.setFinalScore(lastStage != null ? lastStage.getScore() : 0);
+        result.setSuccess(currentContent != null);
+        result.setFeedback(feedback);
+
+        return result;
+    }
+
+    // ═══════════════════════════════════════════
+    //  辅助方法
+    // ═══════════════════════════════════════════
+
+    /** 解析评分JSON中的totalScore */
+    private int parseScoreFromResponse(String json) {
+        try {
+            JSONObject obj = JSON.parseObject(json);
+            if (obj.containsKey("totalScore")) {
+                return obj.getIntValue("totalScore");
+            }
+            // 8维度求和
+            int sum = 0;
+            String[] dims = {"relevance", "professionalism", "completeness", "naturalness",
+                             "compliance", "knowledgeConsistency", "goalAlignment", "humanLikeliness"};
+            for (String dim : dims) {
+                sum += obj.getIntValue(dim);
+            }
+            return sum;
+        } catch (Exception e) {
+            return 0;
+        }
+    }
+
+    /** 提取评分反馈 */
+    private String extractFeedback(String json) {
+        try {
+            JSONObject obj = JSON.parseObject(json);
+            if (obj.containsKey("suggestions")) {
+                Object suggestions = obj.get("suggestions");
+                if (suggestions instanceof String) {
+                    return (String) suggestions;
+                }
+                return JSON.toJSONString(suggestions);
+            }
+            if (obj.containsKey("feedback")) {
+                return obj.getString("feedback");
+            }
+            return json;
+        } catch (Exception e) {
+            return json;
+        }
+    }
+
+    /** 默认评分提示词 */
+    private String buildDefaultScoringPrompt(String content) {
+        return "请按以下8维度对内容评分(每维度0-20分,满分160):\n" +
+            "relevance(相关性), professionalism(专业性), completeness(完整性), naturalness(自然度),\n" +
+            "compliance(合规性), knowledgeConsistency(知识库一致性), goalAlignment(目标对齐性), humanLikeliness(拟人度)\n\n" +
+            "内容:\n" + content + "\n\n" +
+            "输出JSON:{\"relevance\":N,\"professionalism\":N,\"completeness\":N,\"naturalness\":N," +
+            "\"compliance\":N,\"knowledgeConsistency\":N,\"goalAlignment\":N,\"humanLikeliness\":N," +
+            "\"totalScore\":N,\"feedback\":\"改进建议\"}";
+    }
+
+    // ═══════════════════════════════════════════
+    //  内部类 / 接口
+    // ═══════════════════════════════════════════
+
+    /** 评分提示词构建器 */
+    @FunctionalInterface
+    public interface ScoringPromptBuilder {
+        String build(String content);
+    }
+
+    /** 重新生成提示词构建器 */
+    @FunctionalInterface
+    public interface RegeneratePromptBuilder {
+        String build(String originalContent, String feedback);
+    }
+
+    /** 顺序流水线阶段结果 */
+    public static class PipelineStageResult {
+        private int stageIndex;
+        private String modelName;
+        private String modelIdentifier;
+        private String output;
+        private Integer promptTokens;
+        private Integer completionTokens;
+        private boolean success;
+        private String error;
+
+        public int getStageIndex() { return stageIndex; }
+        public void setStageIndex(int stageIndex) { this.stageIndex = stageIndex; }
+        public String getModelName() { return modelName; }
+        public void setModelName(String modelName) { this.modelName = modelName; }
+        public String getModelIdentifier() { return modelIdentifier; }
+        public void setModelIdentifier(String modelIdentifier) { this.modelIdentifier = modelIdentifier; }
+        public String getOutput() { return output; }
+        public void setOutput(String output) { this.output = output; }
+        public Integer getPromptTokens() { return promptTokens; }
+        public void setPromptTokens(Integer promptTokens) { this.promptTokens = promptTokens; }
+        public Integer getCompletionTokens() { return completionTokens; }
+        public void setCompletionTokens(Integer completionTokens) { this.completionTokens = completionTokens; }
+        public boolean isSuccess() { return success; }
+        public void setSuccess(boolean success) { this.success = success; }
+        public String getError() { return error; }
+        public void setError(String error) { this.error = error; }
+    }
+
+    /** 顺序流水线执行结果 */
+    public static class SequentialPipelineResult {
+        private boolean success;
+        private String finalOutput;
+        private List<PipelineStageResult> stages = new ArrayList<>();
+
+        public boolean isSuccess() { return success; }
+        public void setSuccess(boolean success) { this.success = success; }
+        public String getFinalOutput() { return finalOutput; }
+        public void setFinalOutput(String finalOutput) { this.finalOutput = finalOutput; }
+        public List<PipelineStageResult> getStages() { return stages; }
+        public void setStages(List<PipelineStageResult> stages) { this.stages = stages; }
+    }
+}

+ 43 - 1
fs-service/src/main/java/com/fs/company/service/llm/impl/MultiModelRouterImpl.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.company.service.ai.AiModelGateway;
 import com.fs.company.service.ai.AiProviderManager;
+import com.fs.company.service.ai.AiSceneDispatcher;
 import com.fs.company.service.llm.MultiModelRouter;
 import com.fs.company.service.llm.ModelResponse;
 import org.slf4j.Logger;
@@ -13,6 +14,12 @@ import org.springframework.stereotype.Service;
 
 import java.util.*;
 
+/**
+ * 多模型路由实现(改造版:委托给 AiSceneDispatcher)
+ * <p>
+ * 保留原有接口用于向后兼容,实际由 AiSceneDispatcher 根据场景编码分配模型。
+ * systemPrompt 参数被用于推断对应的场景编码。
+ */
 @Service
 public class MultiModelRouterImpl implements MultiModelRouter {
 
@@ -21,21 +28,56 @@ public class MultiModelRouterImpl implements MultiModelRouter {
     @Autowired
     private AiModelGateway aiModelGateway;
 
+    @Autowired(required = false)
+    private AiSceneDispatcher sceneDispatcher;
+
+    /**
+     * systemPrompt → sceneCode 映射表
+     * 用于向后兼容:旧代码传 systemPrompt 作为任务类型提示
+     */
+    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");
+    }
+
+    /** 根据 systemPrompt 推断场景编码 */
+    private String resolveSceneCode(String systemPrompt) {
+        if (systemPrompt != null && HINT_TO_SCENE.containsKey(systemPrompt)) {
+            return HINT_TO_SCENE.get(systemPrompt);
+        }
+        return "workflow_llm"; // 默认场景
+    }
+
     @Override
     public String generateResponse(String prompt, String model, String systemPrompt) {
+        // 优先使用新的场景分发器
+        if (sceneDispatcher != null) {
+            String sceneCode = resolveSceneCode(systemPrompt);
+            return sceneDispatcher.dispatch(prompt, sceneCode, systemPrompt);
+        }
+        // 降级:使用旧网关
         ModelResponse resp = aiModelGateway.chatWithTokens(prompt, systemPrompt, model);
         return resp != null ? resp.getContent() : "";
     }
 
     @Override
     public ModelResponse generateWithTokens(String prompt, String model, String systemPrompt) {
+        if (sceneDispatcher != null) {
+            String sceneCode = resolveSceneCode(systemPrompt);
+            return sceneDispatcher.dispatchWithTokens(prompt, sceneCode, systemPrompt);
+        }
         return aiModelGateway.chatWithTokens(prompt, systemPrompt, model);
     }
 
     @Override
     public String generateWithModelConfig(String prompt, String modelConfig, String systemPrompt) {
         if (modelConfig == null || modelConfig.isEmpty()) {
-            return aiModelGateway.chat(prompt, systemPrompt, null);
+            return generateResponse(prompt, null, systemPrompt);
         }
         try {
             JSONObject config = JSON.parseObject(modelConfig);

+ 116 - 99
fs-service/src/main/java/com/fs/company/service/workflow/impl/MultiModelWorkflowGeneratorImpl.java

@@ -3,8 +3,11 @@ package com.fs.company.service.workflow.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.company.domain.AdminAiModel;
 import com.fs.company.domain.LobsterWorkflowNodeType;
-import com.fs.company.service.llm.MultiModelRouter;
+import com.fs.company.service.ai.AiSceneDispatcher;
+import com.fs.company.service.ai.AdminAiSceneService;
+import com.fs.company.service.ai.MultiModelPipelineEngine;
 import com.fs.company.service.workflow.LobsterNodeTypeService;
 import com.fs.company.service.workflow.LobsterModelConfigService;
 import com.fs.company.service.workflow.MultiModelWorkflowGenerator;
@@ -25,8 +28,17 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
     private static final Logger logger = LoggerFactory.getLogger(MultiModelWorkflowGeneratorImpl.class);
 
+    /** 工作流生成场景编码 */
+    private static final String SCENE_WORKFLOW_GENERATION = "workflow_generation";
+
     @Autowired
-    private MultiModelRouter multiModelRouter;
+    private AiSceneDispatcher sceneDispatcher;
+
+    @Autowired(required = false)
+    private MultiModelPipelineEngine pipelineEngine;
+
+    @Autowired(required = false)
+    private AdminAiSceneService sceneService;
 
     @Autowired(required = false)
     private LobsterNodeTypeService nodeTypeService;
@@ -43,24 +55,6 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
     @Value("${ai.multi-model.default:doubao-lite}")
     private String defaultModel;
 
-    /** 模型解析:DB配置 > yml配置 > 硬编码降级 */
-    private ModelConfig resolveModelConfig(Long companyId, ModelConfig inputConfig) {
-        if (companyId != null && modelConfigService != null) {
-            try {
-                ModelConfig dbConfig = modelConfigService.getWorkflowGeneratorConfig(companyId);
-                if (dbConfig != null && dbConfig.getModelA() != null && !dbConfig.getModelA().isEmpty()) {
-                    return dbConfig;
-                }
-            } catch (Exception e) {
-                logger.debug("[MultiModelWorkflow] DB模型配置查询失败: {}", e.getMessage());
-            }
-        }
-        if (inputConfig != null && inputConfig.getModelA() != null && !inputConfig.getModelA().isEmpty()) {
-            return inputConfig;
-        }
-        return new ModelConfig(defaultModel, defaultModel, defaultModel);
-    }
-
     /* ============ 行业场景规则 ============ */
     private static final Map<String, String> INDUSTRY_RULES = new LinkedHashMap<>();
     static {
@@ -101,39 +95,80 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
     public GenerationResult generateWorkflowWithResult(Long companyId, String requirement, String industryType, ModelConfig modelConfig) {
         try {
-            logger.info("[MultiModelWorkflow] Starting multi-model workflow generation...");
+            logger.info("[MultiModelWorkflow] Starting multi-model workflow generation via pipeline engine...");
 
-            ModelConfig resolved = resolveModelConfig(companyId, modelConfig);
             String dynamicNodeTypes = buildDynamicNodeTypeList();
             String industryRule = getIndustryRule(companyId, industryType);
 
-            String modelAOutput = generateInitialDraft(resolved.getModelA(), requirement, industryType, dynamicNodeTypes, industryRule);
+            // 从场景中获取排序后的模型列表
+            List<AdminAiModel> models = (sceneService != null)
+                ? sceneService.getEnabledModels(SCENE_WORKFLOW_GENERATION)
+                : Collections.emptyList();
+
+            // 构建3个阶段的提示词
+            List<String> prompts = new ArrayList<>();
+            prompts.add(buildGeneratePrompt(requirement, industryType, dynamicNodeTypes, industryRule));
+            prompts.add(buildImprovePrompt(requirement, dynamicNodeTypes, "【占位-将替换为模型A输出】"));
+            prompts.add(buildValidatePrompt(dynamicNodeTypes, "【占位-将替换为模型B输出】"));
+
+            List<String> systemPrompts = Arrays.asList(
+                "workflow_generator", "workflow_improver", "workflow_validator");
+
+            String fallback = buildDefaultWorkflow(requirement, industryType);
+
+            // 如果有pipeline引擎且模型≥1,使用流水线引擎
+            if (pipelineEngine != null && models.size() >= 3) {
+                // 为阶段1和2构建正确的prompt(需要阶段0的输出,这里先用初始prompt)
+                // 流水线引擎会自动将当前输出作为后续阶段的上下文
+                MultiModelPipelineEngine.SequentialPipelineResult pipeResult =
+                    pipelineEngine.executeSequential(models, prompts, systemPrompts, fallback);
+
+                if (!pipeResult.isSuccess() || pipeResult.getFinalOutput() == null) {
+                    return GenerationResult.success(fallback, null, null, null, "70", "流水线执行失败,使用默认模板");
+                }
+
+                String modelAOutput = repairJson(pipeResult.getStages().size() > 0 ?
+                    pipeResult.getStages().get(0).getOutput() : "");
+                String modelBOutput = repairJson(pipeResult.getStages().size() > 1 ?
+                    pipeResult.getStages().get(1).getOutput() : modelAOutput);
+                String finalWorkflow = pipeResult.getFinalOutput();
+                finalWorkflow = repairJson(finalWorkflow);
+
+                autoGeneratePrompts(companyId, industryType, finalWorkflow, requirement);
+                return GenerationResult.success(finalWorkflow, modelAOutput, modelBOutput,
+                    "Pipeline executed", "85", "流水线模式完成");
+            }
+
+            // ── 降级:单模型场景或旧模式 ──
+            // 使用场景分发器执行三阶段
+            String modelAOutput = sceneDispatcher.dispatch(
+                buildGeneratePrompt(requirement, industryType, dynamicNodeTypes, industryRule),
+                SCENE_WORKFLOW_GENERATION, "workflow_generator");
             if (modelAOutput == null || modelAOutput.isEmpty()) {
                 logger.warn("[MultiModelWorkflow] Model A failed, using default workflow");
-                return GenerationResult.success(buildDefaultWorkflow(requirement, industryType),
-                        null, null, null, "70", "模型A生成失败,使用默认模板");
+                return GenerationResult.success(fallback, null, null, null, "70", "模型A生成失败");
             }
 
             modelAOutput = repairJson(modelAOutput);
-            logger.info("[MultiModelWorkflow] Phase 1 completed (after JSON repair)");
+            logger.info("[MultiModelWorkflow] Phase 1 completed");
 
-            String modelBOutput = improveWorkflow(resolved.getModelB(), modelAOutput, requirement, dynamicNodeTypes);
+            String modelBOutput = sceneDispatcher.dispatch(
+                buildImprovePrompt(requirement, dynamicNodeTypes, modelAOutput),
+                SCENE_WORKFLOW_GENERATION, "workflow_improver");
             if (modelBOutput == null || modelBOutput.isEmpty()) {
                 modelBOutput = modelAOutput;
                 logger.warn("[MultiModelWorkflow] Model B failed, using Model A output");
             }
 
             modelBOutput = repairJson(modelBOutput);
-            logger.info("[MultiModelWorkflow] Phase 2 completed (after JSON repair)");
-
-            ValidationResult validation = validateWorkflow(resolved.getModelC(), modelBOutput, dynamicNodeTypes);
-            logger.info("[MultiModelWorkflow] Phase 3 completed, score: {}", validation.score);
+            String validateResp = sceneDispatcher.dispatch(
+                buildValidatePrompt(dynamicNodeTypes, modelBOutput),
+                SCENE_WORKFLOW_GENERATION, "workflow_validator");
 
-            String finalWorkflow = validation.passed ? modelBOutput : buildDefaultWorkflow(requirement, industryType);
+            ValidationResult validation = parseValidation(validateResp);
+            String finalWorkflow = validation.passed ? modelBOutput : fallback;
 
-            /* 自动生成租户+行业专属Prompt */
             autoGeneratePrompts(companyId, industryType, finalWorkflow, requirement);
-
             return GenerationResult.success(finalWorkflow, modelAOutput, modelBOutput,
                     validation.details, validation.score, JSON.toJSONString(validation.suggestions));
 
@@ -145,8 +180,6 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
 
     /**
      * AI迭代优化:基于已有工作流进行增量修改
-     * @param existingWorkflowJson 已有工作流JSON
-     * @param modifyInstruction 修改指令(如"增加一个关怀节点""删除AI识别节点""调整话术")
      */
     @Override
     public GenerationResult iterateOptimize(Long companyId, String existingWorkflowJson,
@@ -161,13 +194,16 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
                     "可用的节点类型:\n" + dynamicNodeTypes + "\n\n" +
                     "要求: 1.只修改指令要求的部分 2.保持其他节点不变 3.确保节点编码一致性 4.输出纯JSON";
 
-            String improved = multiModelRouter.generateResponse(prompt, modelConfig.getModelA(), "workflow_optimizer");
+            String improved = sceneDispatcher.dispatch(prompt, SCENE_WORKFLOW_GENERATION, "workflow_optimizer");
             if (improved == null || improved.isEmpty()) {
                 return GenerationResult.fail("AI迭代优化未产生结果");
             }
 
             improved = repairJson(improved);
-            validateWorkflow(modelConfig.getModelC(), improved, dynamicNodeTypes);
+            String validateResp = sceneDispatcher.dispatch(
+                buildValidatePrompt(dynamicNodeTypes, improved),
+                SCENE_WORKFLOW_GENERATION, "workflow_validator");
+            ValidationResult vr = parseValidation(validateResp);
 
             return GenerationResult.success(improved, existingWorkflowJson, improved,
                     "迭代优化完成", "85", "根据指令优化: " + modifyInstruction);
@@ -296,74 +332,55 @@ public class MultiModelWorkflowGeneratorImpl implements MultiModelWorkflowGenera
         return "{\"templateName\":\"修复的工作流\",\"nodes\":[{\"nodeCode\":\"START\",\"nodeName\":\"开始\",\"nodeType\":1,\"sortNo\":1,\"nextNodeCode\":\"END\"},{\"nodeCode\":\"END\",\"nodeName\":\"结束\",\"nodeType\":99,\"sortNo\":2}]}";
     }
 
-    /* ============ 核心生成方法 ============ */
-    private String generateInitialDraft(String modelName, String requirement, String industryType,
-                                         String dynamicNodeTypes, String industryRule) {
-        try {
-            String prompt = "你是CRM系统专家级工作流设计师。根据需求描述和行业规则,生成完整的工作流模板。\n\n" +
-                    "【需求描述】\n" + (requirement != null ? requirement : "通用客户跟进流程") + "\n\n" +
-                    "【行业规则 - 必须遵守】\n" + industryRule + "\n\n" +
-                    "【可用节点类型 - 必须使用这些数字】\n" + dynamicNodeTypes + "\n\n" +
-                    "【输出格式 - 纯JSON】\n" +
-                    "{\"templateName\":\"工作流名称\",\"industryType\":\"行业代码\",\"description\":\"描述\",\n" +
-                    " \"variables\":[{\"name\":\"变量名\",\"label\":\"标签\",\"type\":\"string|number|list\"}],\n" +
-                    " \"nodes\":[\n" +
-                    "  {\"nodeCode\":\"唯一编码\",\"nodeName\":\"节点名称\",\"nodeType\":数字,\n" +
-                    "   \"sortNo\":序号,\"nextNodeCode\":\"下一节点编码\",\n" +
-                    "   \"messageTemplate\":\"话术模板(支持${变量})\",\"conditionExpr\":\"条件或空\",\n" +
-                    "   \"nodeConfig\":\"节点配置JSON\",\"maxRounds\":0}\n" +
-                    " ],\n" +
-                    " \"edges\":[{\"sourceNodeCode\":\"源\",\"targetNodeCode\":\"目标\",\"edgeLabel\":\"标签\"}]\n" +
-                    "}\n\n" +
-                    "要求: 1.最少3个节点 2.nodeCode唯一 3.最后一个节点nextNodeCode为空 4.输出纯JSON无其他文字";
-
-            return multiModelRouter.generateResponse(prompt, modelName, "workflow_generator");
-
-        } catch (Exception e) {
-            logger.warn("[MultiModelWorkflow] Model A generation failed: {}", e.getMessage());
-            return null;
-        }
+    /* ============ 提示词构建方法(纯字符串构建,不调用模型) ============ */
+
+    private String buildGeneratePrompt(String requirement, String industryType,
+                                        String dynamicNodeTypes, String industryRule) {
+        return "你是CRM系统专家级工作流设计师。根据需求描述和行业规则,生成完整的工作流模板。\n\n" +
+                "【需求描述】\n" + (requirement != null ? requirement : "通用客户跟进流程") + "\n\n" +
+                "【行业规则 - 必须遵守】\n" + industryRule + "\n\n" +
+                "【可用节点类型 - 必须使用这些数字】\n" + dynamicNodeTypes + "\n\n" +
+                "【输出格式 - 纯JSON】\n" +
+                "{\"templateName\":\"工作流名称\",\"industryType\":\"行业代码\",\"description\":\"描述\",\n" +
+                " \"variables\":[{\"name\":\"变量名\",\"label\":\"标签\",\"type\":\"string|number|list\"}],\n" +
+                " \"nodes\":[\n" +
+                "  {\"nodeCode\":\"唯一编码\",\"nodeName\":\"节点名称\",\"nodeType\":数字,\n" +
+                "   \"sortNo\":序号,\"nextNodeCode\":\"下一节点编码\",\n" +
+                "   \"messageTemplate\":\"话术模板(支持${变量})\",\"conditionExpr\":\"条件或空\",\n" +
+                "   \"nodeConfig\":\"节点配置JSON\",\"maxRounds\":0}\n" +
+                " ],\n" +
+                " \"edges\":[{\"sourceNodeCode\":\"源\",\"targetNodeCode\":\"目标\",\"edgeLabel\":\"标签\"}]\n" +
+                "}\n\n" +
+                "要求: 1.最少3个节点 2.nodeCode唯一 3.最后一个节点nextNodeCode为空 4.输出纯JSON无其他文字";
     }
 
-    private String improveWorkflow(String modelName, String draftJson, String requirement, String dynamicNodeTypes) {
-        try {
-            String prompt = "你是工作流优化专家。审查并完善这个工作流草稿,检查:\n" +
-                    "1. 节点连接是否合理 2. 话术模板是否完整 3. 条件表达式是否正确 4. 变量是否定义\n\n" +
-                    "原始需求: " + (requirement != null ? requirement : "") + "\n\n" +
-                    "可用节点类型: " + dynamicNodeTypes + "\n\n" +
-                    "工作流草稿:\n" + draftJson + "\n\n" +
-                    "输出纯JSON(只输出改进后的工作流,无其他文字):";
-
-            return multiModelRouter.generateResponse(prompt, modelName, "workflow_improver");
+    private String buildImprovePrompt(String requirement, String dynamicNodeTypes, String draftJson) {
+        return "你是工作流优化专家。审查并完善这个工作流草稿,检查:\n" +
+                "1. 节点连接是否合理 2. 话术模板是否完整 3. 条件表达式是否正确 4. 变量是否定义\n\n" +
+                "原始需求: " + (requirement != null ? requirement : "") + "\n\n" +
+                "可用节点类型: " + dynamicNodeTypes + "\n\n" +
+                "工作流草稿:\n" + draftJson + "\n\n" +
+                "输出纯JSON(只输出改进后的工作流,无其他文字):";
+    }
 
-        } catch (Exception e) {
-            logger.warn("[MultiModelWorkflow] Model B improvement failed: {}", e.getMessage());
-            return null;
-        }
+    private String buildValidatePrompt(String dynamicNodeTypes, String workflowJson) {
+        return "你是工作流QA专家。验证以下工作流JSON:\n\n" +
+                "期望节点类型: " + dynamicNodeTypes + "\n\n" +
+                "工作流:\n" + workflowJson + "\n\n" +
+                "检查: 1.节点类型是否在可用范围内 2.nodeCode是否唯一 3.节点是否连通 4.START/END是否存在\n" +
+                "输出JSON: {\"passed\":true/false,\"score\":\"0-100\",\"suggestions\":[\"建议1\"],\"details\":\"详情\"}";
     }
 
-    private ValidationResult validateWorkflow(String modelName, String workflowJson, String dynamicNodeTypes) {
+    private ValidationResult parseValidation(String response) {
         try {
-            String prompt = "你是工作流QA专家。验证以下工作流JSON:\n\n" +
-                    "期望节点类型: " + dynamicNodeTypes + "\n\n" +
-                    "工作流:\n" + workflowJson + "\n\n" +
-                    "检查: 1.节点类型是否在可用范围内 2.nodeCode是否唯一 3.节点是否连通 4.START/END是否存在\n" +
-                    "输出JSON: {\"passed\":true/false,\"score\":\"0-100\",\"suggestions\":[\"建议1\"],\"details\":\"详情\"}";
-
-            String response = multiModelRouter.generateResponse(prompt, modelName, "workflow_validator");
-            try {
-                JSONObject json = JSON.parseObject(repairJson(response));
-                return new ValidationResult(json.getBooleanValue("passed"), json.getString("score"),
-                        json.getString("details"),
-                        json.getJSONArray("suggestions") != null ?
-                                json.getJSONArray("suggestions").toJavaList(String.class) : new ArrayList<>());
-            } catch (Exception e) {
-                return new ValidationResult(true, "85", "Validation completed",
-                        Arrays.asList("No specific suggestions"));
-            }
+            JSONObject json = JSON.parseObject(repairJson(response));
+            return new ValidationResult(json.getBooleanValue("passed"), json.getString("score"),
+                    json.getString("details"),
+                    json.getJSONArray("suggestions") != null ?
+                            json.getJSONArray("suggestions").toJavaList(String.class) : new ArrayList<>());
         } catch (Exception e) {
-            logger.warn("[MultiModelWorkflow] Model C validation failed: {}", e.getMessage());
-            return new ValidationResult(true, "75", "Validation skipped", Arrays.asList("Validation error"));
+            return new ValidationResult(true, "85", "Validation completed",
+                    Arrays.asList("No specific suggestions"));
         }
     }
 

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

@@ -2,54 +2,50 @@ package com.fs.company.service.workflow.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
-import com.fs.company.service.llm.MultiModelRouter;
+import com.fs.company.domain.AdminAiModel;
+import com.fs.company.domain.AdminAiScene;
+import com.fs.company.service.ai.AiSceneDispatcher;
+import com.fs.company.service.ai.AdminAiSceneService;
+import com.fs.company.service.ai.MultiModelPipelineEngine;
 import com.fs.company.service.workflow.QualityScoringService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 
 /**
- * 8维度质量评分服务实现
+ * 8维度质量评分服务实现(改造版:场景驱动多模型流水线)
  *
  * 维度列表(满分160分,每维度20分):
- * 1. relevance(相关性)- 是否直接回答客户问题
- * 2. professionalism(专业性)- 信息是否准确专业
- * 3. completeness(完整性)- 是否充分覆盖客户需求
- * 4. naturalness(自然度)- 语气是否自然有温度
- * 5. compliance(合规性)- 是否包含敏感/不当内容
- * 6. knowledgeConsistency(知识库一致性)- 与知识库事实是否一致
- * 7. goalAlignment(目标对齐性)- 是否推进对话目标
- * 8. humanLikeliness(拟人度)- 回复是否像真人说的,而非机器人
+ * 1. relevance(相关性) 2. professionalism(专业性) 3. completeness(完整性)
+ * 4. naturalness(自然度) 5. compliance(合规性) 6. knowledgeConsistency(知识库一致性)
+ * 7. goalAlignment(目标对齐性) 8. humanLikeliness(拟人度)
  *
- * 拟人度维度的核心目的:
- * 解决AI回复暴露机器人身份的问题。当AI输出过于专业、篇幅过长、
- * 语气过于正式时,客户会意识到在和机器人对话,破坏无感体验。
- * 拟人度低分会触发重生成,要求AI用更口语化、简短的方式回复。
+ * 多模型流水线规则:
+ * - 场景配置1个模型 → 不启用质量评分,直接生成
+ * - 场景配置≥2个模型 → 启用质量评分链
+ * - 偶数索引=generator, 奇数索引=scorer
+ * - 末尾模型降权处理(只生成,不自评分)
  */
 @Service
 public class QualityScoringServiceImpl implements QualityScoringService {
 
     private static final Logger logger = LoggerFactory.getLogger(QualityScoringServiceImpl.class);
 
-    /** 拟人度检测:回复字数超过此阈值,开始扣分 */
-    private static final int LONG_REPLY_THRESHOLD = 200;
+    /** 质量评分场景编码 */
+    private static final String SCENE_QUALITY_SCORING = "quality_scoring";
 
-    /** 拟人度检测:回复字数超过此阈值,严重扣分 */
+    /** 拟人度检测阈值 */
+    private static final int LONG_REPLY_THRESHOLD = 200;
     private static final int VERY_LONG_REPLY_THRESHOLD = 500;
-
-    /** 拟人度检测:过于正式/专业的关键词模式 */
     private static final String[] FORMAL_PATTERNS = {
             "综上所述", "总而言之", "首先其次最后", "第一第二第三",
             "需要注意的是", "在此提醒", "根据相关规定", "按照要求",
             "敬请知悉", "特此通知", "如有疑问请", "现将有关事项",
             "以下是", "如下所示", "详细说明如下", "具体如下"
     };
-
-    /** 拟人度检测:口语化关键词模式(出现则加分) */
     private static final String[] CASUAL_PATTERNS = {
             "嗯", "啊", "呢", "呀", "哈", "嘛", "哦",
             "对的", "没错", "确实", "当然", "放心",
@@ -57,37 +53,82 @@ public class QualityScoringServiceImpl implements QualityScoringService {
     };
 
     @Autowired
-    private MultiModelRouter multiModelRouter;
+    private AiSceneDispatcher sceneDispatcher;
+
+    @Autowired
+    private AdminAiSceneService sceneService;
+
+    @Autowired
+    private MultiModelPipelineEngine pipelineEngine;
 
     @Override
     public ScoringResult scoreWithRetry(Long companyId, String content, String userQuestion,
                                          String knowledgeBase, String conversationGoal, String messageTemplate) {
-        DetailedScore firstScore = score(companyId, content, userQuestion, knowledgeBase, conversationGoal, messageTemplate);
-
-        logger.info("[QualityScoring] First score: {}/{} (threshold: {})",
-                firstScore.getTotalScore(), Threshold.FULL_SCORE, Threshold.FIRST_PASS_THRESHOLD);
-
-        if (firstScore.getTotalScore() >= Threshold.FIRST_PASS_THRESHOLD) {
-            Map<String, Integer> dimensions = buildDimensionMap(firstScore);
-            return ScoringResult.pass(content, firstScore.getTotalScore(), dimensions);
+        // 检查场景模型数量
+        List<AdminAiModel> models = sceneService.getEnabledModels(SCENE_QUALITY_SCORING);
+        int modelCount = models.size();
+
+        // 只有1个模型 → 不启用评分,直接评分返回(保持向后兼容)
+        if (modelCount <= 1) {
+            DetailedScore score = score(companyId, content, userQuestion, knowledgeBase, conversationGoal, messageTemplate);
+            Map<String, Integer> dimensions = buildDimensionMap(score);
+            return ScoringResult.pass(content, score.getTotalScore(), dimensions);
         }
 
-        String feedback = buildFeedback(firstScore, knowledgeBase, conversationGoal, messageTemplate);
-        String regeneratedContent = regenerateContent(content, userQuestion, feedback);
+        // ── ≥2个模型 → 启用评分链 ──
+        AdminAiScene scene = sceneService.getScene(SCENE_QUALITY_SCORING);
+        int threshold = scene != null && scene.getQualityThreshold() != null
+                ? scene.getQualityThreshold() : 120;
 
-        DetailedScore secondScore = score(companyId, regeneratedContent, userQuestion, knowledgeBase, conversationGoal, messageTemplate);
+        // 构建初始生成prompt(将content作为待评分内容)
+        String generatePrompt = buildRegeneratePrompt(content, userQuestion,
+                buildFeedback(buildDefaultQuickScore(content, knowledgeBase, conversationGoal),
+                        knowledgeBase, conversationGoal, messageTemplate));
 
-        logger.info("[QualityScoring] Second score: {}/{} (threshold: {})",
-                secondScore.getTotalScore(), Threshold.FULL_SCORE, Threshold.SECOND_PASS_THRESHOLD);
-
-        if (secondScore.getTotalScore() >= Threshold.SECOND_PASS_THRESHOLD) {
-            Map<String, Integer> dimensions = buildDimensionMap(secondScore);
-            return ScoringResult.passAfterRetry(regeneratedContent, firstScore.getTotalScore(),
-                    secondScore.getTotalScore(), dimensions);
+        try {
+            MultiModelPipelineEngine.ScoringPipelineResult pipeResult =
+                pipelineEngine.executeScoring(
+                    models,
+                    generatePrompt,
+                    // 评分prompt构造器
+                    genContent -> buildScoringPrompt(genContent, userQuestion, knowledgeBase,
+                            conversationGoal, messageTemplate),
+                    // 重新生成prompt构造器
+                    (origContent, feedback) -> buildRegeneratePrompt(origContent, userQuestion, feedback),
+                    threshold,
+                    content  // 兜底:使用原始content
+                );
+
+            if (pipeResult.isSuccess()) {
+                Map<String, Integer> dimensions = new HashMap<>();
+                // 有评分则返回评分维度的Map
+                if (pipeResult.isScoringEnabled() && !pipeResult.getStages().isEmpty()) {
+                    // 取最后一个评分阶段的维度
+                    for (MultiModelPipelineEngine.ScoringStageResult stage : pipeResult.getStages()) {
+                        if ("scorer".equals(stage.getRole()) && stage.getContent() != null) {
+                            try {
+                                JSONObject json = JSON.parseObject(stage.getContent());
+                                dimensions.put("relevance", json.getIntValue("relevance"));
+                                dimensions.put("professionalism", json.getIntValue("professionalism"));
+                                dimensions.put("completeness", json.getIntValue("completeness"));
+                                dimensions.put("naturalness", json.getIntValue("naturalness"));
+                                dimensions.put("compliance", json.getIntValue("compliance"));
+                                dimensions.put("knowledgeConsistency", json.getIntValue("knowledgeConsistency"));
+                                dimensions.put("goalAlignment", json.getIntValue("goalAlignment"));
+                                dimensions.put("humanLikeliness", json.getIntValue("humanLikeliness"));
+                            } catch (Exception ignored) {}
+                            break;
+                        }
+                    }
+                }
+                return ScoringResult.pass(pipeResult.getFinalContent(), pipeResult.getFinalScore(), dimensions);
+            } else {
+                return ScoringResult.fail(content, 0, 0, "评分流水线失败,使用原始回复");
+            }
+        } catch (Exception e) {
+            logger.error("[QualityScoring] Pipeline scoring failed: {}", e.getMessage(), e);
+            return ScoringResult.fail(content, 0, 0, "评分流水线异常: " + e.getMessage());
         }
-
-        return ScoringResult.fail(content, firstScore.getTotalScore(),
-                secondScore.getTotalScore(), "两次评分均未达标,使用原始回复");
     }
 
     @Override
@@ -95,7 +136,7 @@ public class QualityScoringServiceImpl implements QualityScoringService {
                                String knowledgeBase, String conversationGoal, String messageTemplate) {
         try {
             String prompt = buildScoringPrompt(content, userQuestion, knowledgeBase, conversationGoal, messageTemplate);
-            String response = multiModelRouter.generateResponse(prompt, null, "quality_scorer");
+            String response = sceneDispatcher.dispatch(prompt, SCENE_QUALITY_SCORING, null);
 
             DetailedScore result = parseScoreResponse(response, knowledgeBase, conversationGoal);
 
@@ -326,22 +367,44 @@ public class QualityScoringServiceImpl implements QualityScoringService {
         return feedback.toString();
     }
 
+    /** 快速构建默认评分(不涉及内容长度,用于初始化反馈) */
+    private DetailedScore buildDefaultQuickScore(String content, String knowledgeBase, String conversationGoal) {
+        DetailedScore score = new DetailedScore();
+        score.setRelevance(12);
+        score.setProfessionalism(12);
+        score.setCompleteness(12);
+        score.setNaturalness(13);
+        score.setCompliance(18);
+        score.setKnowledgeConsistency(knowledgeBase != null && !knowledgeBase.isEmpty() ? 12 : 15);
+        score.setGoalAlignment(conversationGoal != null && !conversationGoal.isEmpty() ? 12 : 15);
+        score.setHumanLikeliness(12);
+        score.setTotalScore(score.getRelevance() + score.getProfessionalism() + score.getCompleteness() +
+                score.getNaturalness() + score.getCompliance() + score.getKnowledgeConsistency() +
+                score.getGoalAlignment() + score.getHumanLikeliness());
+        return score;
+    }
+
+    /** 构建重新生成提示词(不调用模型,仅构建prompt字符串) */
+    private String buildRegeneratePrompt(String originalContent, String userQuestion, String feedback) {
+        StringBuilder prompt = new StringBuilder();
+        prompt.append("请根据以下反馈重新优化AI回复:\n\n");
+        prompt.append("原始回复:").append(originalContent).append("\n\n");
+        prompt.append("用户问题:").append(userQuestion != null ? userQuestion : "无").append("\n\n");
+        prompt.append("优化建议:\n").append(feedback).append("\n\n");
+        prompt.append("重要要求:\n");
+        prompt.append("1. 像微信好友聊天一样回复,语气口语化\n");
+        prompt.append("2. 控制篇幅,一般不超过150字\n");
+        prompt.append("3. 不要用编号列表、分点论述\n");
+        prompt.append("4. 不要用\"综上所述\"等书面表达\n");
+        prompt.append("5. 适当加入口语化语气词\n\n");
+        prompt.append("请输出优化后的回复内容:");
+        return prompt.toString();
+    }
+
     private String regenerateContent(String originalContent, String userQuestion, String feedback) {
         try {
-            StringBuilder prompt = new StringBuilder();
-            prompt.append("请根据以下反馈重新优化AI回复:\n\n");
-            prompt.append("原始回复:").append(originalContent).append("\n\n");
-            prompt.append("用户问题:").append(userQuestion != null ? userQuestion : "无").append("\n\n");
-            prompt.append("优化建议:\n").append(feedback).append("\n\n");
-            prompt.append("重要要求:\n");
-            prompt.append("1. 像微信好友聊天一样回复,语气口语化\n");
-            prompt.append("2. 控制篇幅,一般不超过150字\n");
-            prompt.append("3. 不要用编号列表、分点论述\n");
-            prompt.append("4. 不要用\"综上所述\"等书面表达\n");
-            prompt.append("5. 适当加入口语化语气词\n\n");
-            prompt.append("请输出优化后的回复内容:");
-
-            return multiModelRouter.generateResponse(prompt.toString(), null, "content_optimizer");
+            String prompt = buildRegeneratePrompt(originalContent, userQuestion, feedback);
+            return sceneDispatcher.dispatch(prompt, SCENE_QUALITY_SCORING, null);
         } catch (Exception e) {
             logger.error("[QualityScoring] Regenerate content failed: {}", e.getMessage());
             return originalContent;

+ 95 - 0
fs-service/src/main/resources/mapper/company/AdminAiModelMapper.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.AdminAiModelMapper">
+
+    <resultMap type="com.fs.company.domain.AdminAiModel" id="AdminAiModelResult">
+        <id property="id" column="id"/>
+        <result property="modelName" column="model_name"/>
+        <result property="providerCode" column="provider_code"/>
+        <result property="modelIdentifier" column="model_identifier"/>
+        <result property="apiEndpoint" column="api_endpoint"/>
+        <result property="apiKey" column="api_key"/>
+        <result property="embeddingEndpoint" column="embedding_endpoint"/>
+        <result property="maxTokens" column="max_tokens"/>
+        <result property="temperature" column="temperature"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectVo">
+        select id, model_name, provider_code, model_identifier, api_endpoint, api_key,
+               embedding_endpoint, max_tokens, temperature, sort_order, status,
+               create_by, create_time, update_by, update_time
+        from admin_ai_model
+    </sql>
+
+    <select id="selectList" resultMap="AdminAiModelResult">
+        <include refid="selectVo"/>
+        order by sort_order asc, id asc
+    </select>
+
+    <select id="selectById" resultMap="AdminAiModelResult">
+        <include refid="selectVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectEnabled" resultMap="AdminAiModelResult">
+        <include refid="selectVo"/>
+        where status = 1
+        order by sort_order asc, id asc
+    </select>
+
+    <select id="selectBySceneCode" resultMap="AdminAiModelResult">
+        select m.id, m.model_name, m.provider_code, m.model_identifier, m.api_endpoint, m.api_key,
+               m.embedding_endpoint, m.max_tokens, m.temperature, m.sort_order, m.status,
+               m.create_by, m.create_time, m.update_by, m.update_time
+        from admin_ai_model m
+        inner join admin_ai_scene_model sm on m.id = sm.model_id
+        where sm.scene_code = #{sceneCode} and m.status = 1
+        order by sm.pipeline_order asc, sm.sort_weight asc
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        insert into admin_ai_model (
+            model_name, provider_code, model_identifier, api_endpoint, api_key,
+            embedding_endpoint, max_tokens, temperature, sort_order, status,
+            create_by, create_time, update_by, update_time
+        ) values (
+            #{modelName}, #{providerCode}, #{modelIdentifier}, #{apiEndpoint}, #{apiKey},
+            #{embeddingEndpoint}, #{maxTokens}, #{temperature}, #{sortOrder}, #{status},
+            #{createBy}, #{createTime}, #{updateBy}, #{updateTime}
+        )
+    </insert>
+
+    <update id="updateById">
+        update admin_ai_model
+        <set>
+            <if test="modelName != null">model_name = #{modelName},</if>
+            <if test="providerCode != null">provider_code = #{providerCode},</if>
+            <if test="modelIdentifier != null">model_identifier = #{modelIdentifier},</if>
+            <if test="apiEndpoint != null">api_endpoint = #{apiEndpoint},</if>
+            <if test="apiKey != null">api_key = #{apiKey},</if>
+            <if test="embeddingEndpoint != null">embedding_endpoint = #{embeddingEndpoint},</if>
+            <if test="maxTokens != null">max_tokens = #{maxTokens},</if>
+            <if test="temperature != null">temperature = #{temperature},</if>
+            <if test="sortOrder != null">sort_order = #{sortOrder},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            update_time = #{updateTime}
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        delete from admin_ai_model where id = #{id}
+    </delete>
+
+    <update id="updateSortOrder">
+        update admin_ai_model set sort_order = #{sortOrder} where id = #{id}
+    </update>
+
+</mapper>

+ 68 - 0
fs-service/src/main/resources/mapper/company/AdminAiSceneMapper.xml

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.AdminAiSceneMapper">
+
+    <resultMap type="com.fs.company.domain.AdminAiScene" id="AdminAiSceneResult">
+        <id property="id" column="id"/>
+        <result property="sceneCode" column="scene_code"/>
+        <result property="sceneName" column="scene_name"/>
+        <result property="sceneType" column="scene_type"/>
+        <result property="pipelineType" column="pipeline_type"/>
+        <result property="qualityThreshold" column="quality_threshold"/>
+        <result property="status" column="status"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="selectVo">
+        select id, scene_code, scene_name, scene_type, pipeline_type, quality_threshold,
+               status, create_by, create_time, update_by, update_time
+        from admin_ai_scene
+    </sql>
+
+    <select id="selectList" resultMap="AdminAiSceneResult">
+        <include refid="selectVo"/>
+        order by id asc
+    </select>
+
+    <select id="selectById" resultMap="AdminAiSceneResult">
+        <include refid="selectVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectByCode" resultMap="AdminAiSceneResult">
+        <include refid="selectVo"/>
+        where scene_code = #{sceneCode} and status = 1
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        insert into admin_ai_scene (
+            scene_code, scene_name, scene_type, pipeline_type, quality_threshold,
+            status, create_by, create_time, update_by, update_time
+        ) values (
+            #{sceneCode}, #{sceneName}, #{sceneType}, #{pipelineType}, #{qualityThreshold},
+            #{status}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}
+        )
+    </insert>
+
+    <update id="updateById">
+        update admin_ai_scene
+        <set>
+            <if test="sceneName != null">scene_name = #{sceneName},</if>
+            <if test="sceneType != null">scene_type = #{sceneType},</if>
+            <if test="pipelineType != null">pipeline_type = #{pipelineType},</if>
+            <if test="qualityThreshold != null">quality_threshold = #{qualityThreshold},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            update_time = #{updateTime}
+        </set>
+        where id = #{id}
+    </update>
+
+    <update id="updateThreshold">
+        update admin_ai_scene set quality_threshold = #{threshold} where scene_code = #{sceneCode}
+    </update>
+
+</mapper>

+ 95 - 0
fs-service/src/main/resources/mapper/company/AdminAiSceneModelMapper.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.AdminAiSceneModelMapper">
+
+    <resultMap type="com.fs.company.domain.AdminAiSceneModel" id="AdminAiSceneModelResult">
+        <id property="id" column="id"/>
+        <result property="sceneCode" column="scene_code"/>
+        <result property="modelId" column="model_id"/>
+        <result property="pipelineOrder" column="pipeline_order"/>
+        <result property="role" column="role"/>
+        <result property="sortWeight" column="sort_weight"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <resultMap type="com.fs.company.domain.AdminAiSceneModel" id="AdminAiSceneModelWithModelResult" extends="AdminAiSceneModelResult">
+        <association property="model" javaType="com.fs.company.domain.AdminAiModel">
+            <id property="id" column="m_id"/>
+            <result property="modelName" column="m_model_name"/>
+            <result property="providerCode" column="m_provider_code"/>
+            <result property="modelIdentifier" column="m_model_identifier"/>
+            <result property="apiEndpoint" column="m_api_endpoint"/>
+            <result property="apiKey" column="m_api_key"/>
+            <result property="embeddingEndpoint" column="m_embedding_endpoint"/>
+            <result property="maxTokens" column="m_max_tokens"/>
+            <result property="temperature" column="m_temperature"/>
+            <result property="sortOrder" column="m_sort_order"/>
+            <result property="status" column="m_status"/>
+        </association>
+    </resultMap>
+
+    <select id="selectBySceneCode" resultMap="AdminAiSceneModelWithModelResult">
+        select sm.id, sm.scene_code, sm.model_id, sm.pipeline_order, sm.role, sm.sort_weight, sm.create_time,
+               m.id as m_id, m.model_name as m_model_name, m.provider_code as m_provider_code,
+               m.model_identifier as m_model_identifier, m.api_endpoint as m_api_endpoint,
+               m.api_key as m_api_key, m.embedding_endpoint as m_embedding_endpoint,
+               m.max_tokens as m_max_tokens, m.temperature as m_temperature,
+               m.sort_order as m_sort_order, m.status as m_status
+        from admin_ai_scene_model sm
+        inner join admin_ai_model m on sm.model_id = m.id
+        where sm.scene_code = #{sceneCode}
+        order by sm.pipeline_order asc, sm.sort_weight asc
+    </select>
+
+    <select id="selectEnabledBySceneCode" resultMap="AdminAiSceneModelWithModelResult">
+        select sm.id, sm.scene_code, sm.model_id, sm.pipeline_order, sm.role, sm.sort_weight, sm.create_time,
+               m.id as m_id, m.model_name as m_model_name, m.provider_code as m_provider_code,
+               m.model_identifier as m_model_identifier, m.api_endpoint as m_api_endpoint,
+               m.api_key as m_api_key, m.embedding_endpoint as m_embedding_endpoint,
+               m.max_tokens as m_max_tokens, m.temperature as m_temperature,
+               m.sort_order as m_sort_order, m.status as m_status
+        from admin_ai_scene_model sm
+        inner join admin_ai_model m on sm.model_id = m.id
+        where sm.scene_code = #{sceneCode} and m.status = 1
+        order by sm.pipeline_order asc, sm.sort_weight asc
+    </select>
+
+    <select id="selectAll" resultMap="AdminAiSceneModelWithModelResult">
+        select sm.id, sm.scene_code, sm.model_id, sm.pipeline_order, sm.role, sm.sort_weight, sm.create_time,
+               m.id as m_id, m.model_name as m_model_name, m.provider_code as m_provider_code,
+               m.model_identifier as m_model_identifier, m.api_endpoint as m_api_endpoint,
+               m.api_key as m_api_key, m.embedding_endpoint as m_embedding_endpoint,
+               m.max_tokens as m_max_tokens, m.temperature as m_temperature,
+               m.sort_order as m_sort_order, m.status as m_status
+        from admin_ai_scene_model sm
+        inner join admin_ai_model m on sm.model_id = m.id
+        order by sm.scene_code, sm.pipeline_order asc, sm.sort_weight asc
+    </select>
+
+    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
+        insert into admin_ai_scene_model (
+            scene_code, model_id, pipeline_order, role, sort_weight, create_time
+        ) values (
+            #{sceneCode}, #{modelId}, #{pipelineOrder}, #{role}, #{sortWeight}, NOW()
+        )
+    </insert>
+
+    <delete id="deleteById">
+        delete from admin_ai_scene_model where id = #{id}
+    </delete>
+
+    <delete id="deleteBySceneCode">
+        delete from admin_ai_scene_model where scene_code = #{sceneCode}
+    </delete>
+
+    <delete id="deleteBySceneAndModel">
+        delete from admin_ai_scene_model where scene_code = #{sceneCode} and model_id = #{modelId}
+    </delete>
+
+    <update id="updateOrder">
+        update admin_ai_scene_model
+        set pipeline_order = #{pipelineOrder}, role = #{role}, sort_weight = #{sortWeight}
+        where id = #{id}
+    </update>
+
+</mapper>

+ 12 - 0
sql/add_missing_admin_menu.sql

@@ -0,0 +1,12 @@
+-- =====================================================
+-- adminUI 总后台菜单补充 - 缺少的条目
+-- 说明:admin_menu_init.sql 已覆盖 51 个菜单项,
+-- 但遗漏了 号码管理(voiceNumber)
+-- =====================================================
+
+-- 号码管理 挂载到 通信管理(2400) 分组下
+INSERT INTO fs_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 (2411, '号码管理', 2400, 11, 'voiceNumber', 'admin/voiceNumber/index', 'C', 'el-icon-phone-outline', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 将新菜单关联到admin角色(role_id=1)
+INSERT INTO fs_role_menu (role_id, menu_id) VALUES (1, 2411);

+ 148 - 0
sql/admin_ai_model_config.sql

@@ -0,0 +1,148 @@
+-- =====================================================
+-- Admin DB: AI模型统一配置表
+-- 将除了FastGPT、TTS、图像/语音以外的所有AI模型配置
+-- 从租户DB迁移到Admin DB统一管理
+-- =====================================================
+
+-- 1. 模型配置表
+DROP TABLE IF EXISTS admin_ai_model;
+CREATE TABLE admin_ai_model (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+    model_name VARCHAR(100) NOT NULL COMMENT '模型显示名称',
+    provider_code VARCHAR(50) NOT NULL COMMENT '供应商编码: doubao/qwen/yuanbao/deepseek',
+    model_identifier VARCHAR(200) NOT NULL COMMENT '模型标识符, 如 doubao-1-5-pro-32k',
+    api_endpoint VARCHAR(500) NOT NULL COMMENT 'API地址',
+    api_key VARCHAR(500) NOT NULL COMMENT 'API密钥',
+    embedding_endpoint VARCHAR(500) COMMENT '嵌入端点(可选)',
+    max_tokens INT DEFAULT 4096 COMMENT '最大Token数',
+    temperature DOUBLE DEFAULT 0.7 COMMENT '温度参数',
+    sort_order INT DEFAULT 0 COMMENT '全局排序,越小越优先',
+    status TINYINT DEFAULT 1 COMMENT '状态: 1启用 0禁用',
+    create_by VARCHAR(50) DEFAULT 'system' COMMENT '创建人',
+    create_time DATETIME DEFAULT NOW() COMMENT '创建时间',
+    update_by VARCHAR(50) DEFAULT 'system' COMMENT '更新人',
+    update_time DATETIME DEFAULT NOW() ON UPDATE NOW() COMMENT '更新时间'
+) COMMENT='AI模型配置表(Admin DB统一管理, 不含FastGPT/TTS/图像/语音)';
+
+-- 2. 场景定义表
+DROP TABLE IF EXISTS admin_ai_scene;
+CREATE TABLE admin_ai_scene (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+    scene_code VARCHAR(50) NOT NULL UNIQUE COMMENT '场景编码',
+    scene_name VARCHAR(100) NOT NULL COMMENT '场景名称',
+    scene_type VARCHAR(20) NOT NULL DEFAULT 'single' COMMENT '场景类型: single单模型 / multi_pipeline多模型流水线',
+    pipeline_type VARCHAR(30) COMMENT '流水线类型: sequential顺序调用 / scoring质量评分链',
+    quality_threshold INT DEFAULT 120 COMMENT '质量评分通过阈值(满分160, 默认120)',
+    status TINYINT DEFAULT 1 COMMENT '状态: 1启用 0禁用',
+    create_by VARCHAR(50) DEFAULT 'system' COMMENT '创建人',
+    create_time DATETIME DEFAULT NOW() COMMENT '创建时间',
+    update_by VARCHAR(50) DEFAULT 'system' COMMENT '更新人',
+    update_time DATETIME DEFAULT NOW() ON UPDATE NOW() COMMENT '更新时间'
+) COMMENT='AI模型使用场景定义';
+
+-- 3. 场景-模型关联表(支持排序)
+DROP TABLE IF EXISTS admin_ai_scene_model;
+CREATE TABLE admin_ai_scene_model (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
+    scene_code VARCHAR(50) NOT NULL COMMENT '关联场景编码',
+    model_id BIGINT NOT NULL COMMENT '关联模型ID',
+    pipeline_order INT DEFAULT 0 COMMENT '流水线中的顺序(min→max)',
+    role VARCHAR(30) DEFAULT 'generator' COMMENT '角色: generator生成者 / scorer评分者',
+    sort_weight INT DEFAULT 0 COMMENT '当多模型同级时的优先级权重',
+    create_time DATETIME DEFAULT NOW() COMMENT '创建时间',
+    INDEX idx_scene_code (scene_code),
+    INDEX idx_model_id (model_id)
+) COMMENT='场景-模型关联(支持排序)';
+
+-- =====================================================
+-- 初始化数据
+-- =====================================================
+
+-- 从 company_ai_provider 迁移现有模型(如存在)
+INSERT INTO admin_ai_model (model_name, provider_code, model_identifier, api_endpoint, api_key, embedding_endpoint, max_tokens, temperature, sort_order, status)
+SELECT 
+    provider_name,
+    provider_code,
+    model_name,
+    api_endpoint,
+    COALESCE(api_key, ''),
+    CASE WHEN provider_code = 'doubao' THEN REPLACE(api_endpoint, '/chat/completions', '/embeddings')
+         WHEN provider_code = 'qwen' THEN REPLACE(api_endpoint, '/chat/completions', '/embeddings')
+         ELSE NULL END,
+    COALESCE(max_tokens, 4096),
+    COALESCE(temperature, 0.7),
+    CASE WHEN is_default = 1 THEN 0 ELSE 10 END,
+    enabled
+FROM company_ai_provider
+WHERE del_flag = 0 AND enabled = 1;
+
+-- 如果上述迁移没有数据,插入默认4种模型预设
+INSERT INTO admin_ai_model (model_name, provider_code, model_identifier, api_endpoint, api_key, embedding_endpoint, max_tokens, temperature, sort_order, status)
+SELECT * FROM (
+    SELECT '豆包(Doubao)' AS model_name, 'doubao' AS provider_code, 'doubao-pro-32k' AS model_identifier, 
+           'https://ark.cn-beijing.volces.com/api/v3/chat/completions' AS api_endpoint, '' AS api_key,
+           'https://ark.cn-beijing.volces.com/api/v3/embeddings' AS embedding_endpoint,
+           4096 AS max_tokens, 0.7 AS temperature, 0 AS sort_order, 1 AS status
+    UNION ALL
+    SELECT '通义千问(Qwen)', 'qwen', 'qwen-plus',
+           'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', '',
+           'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings',
+           8192, 0.7, 1, 1
+    UNION ALL
+    SELECT '元宝/混元(Yuanbao)', 'yuanbao', 'hunyuan-lite',
+           'https://api.hunyuan.cloud.tencent.com/v1/chat/completions', '',
+           NULL, 4096, 0.7, 2, 1
+    UNION ALL
+    SELECT 'DeepSeek', 'deepseek', 'deepseek-chat',
+           'https://api.deepseek.com/v1/chat/completions', '',
+           NULL, 8192, 0.7, 3, 1
+) t
+WHERE NOT EXISTS (SELECT 1 FROM admin_ai_model LIMIT 1);
+
+-- 初始化19个场景
+INSERT INTO admin_ai_scene (scene_code, scene_name, scene_type, pipeline_type, quality_threshold, status) VALUES
+-- 多模型流水线场景
+('workflow_generation', '工作流生成', 'multi_pipeline', 'sequential', NULL, 1),
+('quality_scoring',     '质量评分',   'multi_pipeline', 'scoring',    120, 1),
+-- 单模型场景
+('workflow_llm',        '工作流LLM回答',    'single', NULL, NULL, 1),
+('workflow_fallback',   '工作流兜底回答',   'single', NULL, NULL, 1),
+('dynamic_node',        '动态节点执行',     'single', NULL, NULL, 1),
+('semantic_analysis',   '语义意图分析',     'single', NULL, NULL, 1),
+('summary_generation',  '对话摘要',         'single', NULL, NULL, 1),
+('content_moderation',  '内容审核',         'single', NULL, NULL, 1),
+('multi_turn_dialogue', '多轮对话管理',     'single', NULL, NULL, 1),
+('takeover_detection',  '转人工检测',       'single', NULL, NULL, 1),
+('dynamic_adjustment',  '动态节点调整',     'single', NULL, NULL, 1),
+('workflow_evolution',  '工作流进化',       'single', NULL, NULL, 1),
+('template_evolution',  '模板进化',         'single', NULL, NULL, 1),
+('feedback_evolution',  '反馈驱动进化',     'single', NULL, NULL, 1),
+('event_decision',      '事件决策',         'single', NULL, NULL, 1),
+('user_optimization',   '用户级优化',       'single', NULL, NULL, 1),
+('corpus_analysis',     '语料分析',         'single', NULL, NULL, 1),
+('tenant_learning',     '租户学习',         'single', NULL, NULL, 1),
+('vector_embedding',    '向量嵌入',         'single', NULL, NULL, 1);
+
+-- 为单模型场景绑定默认模型(取排序第一的doubao模型)
+INSERT INTO admin_ai_scene_model (scene_code, model_id, pipeline_order, role, sort_weight)
+SELECT s.scene_code, m.id, 0, 'generator', 0
+FROM admin_ai_scene s
+CROSS JOIN (SELECT id FROM admin_ai_model WHERE provider_code = 'doubao' AND status = 1 ORDER BY sort_order LIMIT 1) m
+WHERE s.scene_type = 'single'
+AND NOT EXISTS (SELECT 1 FROM admin_ai_scene_model sm WHERE sm.scene_code = s.scene_code);
+
+-- 为工作流生成场景绑定默认3模型流水线(如存在≥3个模型)
+INSERT INTO admin_ai_scene_model (scene_code, model_id, pipeline_order, role, sort_weight)
+SELECT 'workflow_generation', m.id, m.rn - 1,
+       CASE WHEN m.rn = 1 THEN 'generator' WHEN m.rn = 2 THEN 'generator' ELSE 'scorer' END,
+       0
+FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY sort_order) AS rn FROM admin_ai_model WHERE status = 1 LIMIT 3) m
+WHERE NOT EXISTS (SELECT 1 FROM admin_ai_scene_model WHERE scene_code = 'workflow_generation');
+
+-- 为质量评分场景绑定默认2模型流水线(如存在≥2个模型)
+INSERT INTO admin_ai_scene_model (scene_code, model_id, pipeline_order, role, sort_weight)
+SELECT 'quality_scoring', m.id, m.rn - 1,
+       CASE WHEN m.rn = 1 THEN 'generator' ELSE 'scorer' END,
+       0
+FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY sort_order) AS rn FROM admin_ai_model WHERE status = 1 LIMIT 2) m
+WHERE NOT EXISTS (SELECT 1 FROM admin_ai_scene_model WHERE scene_code = 'quality_scoring');

+ 22 - 0
sql/check_fix_menu.sql

@@ -0,0 +1,22 @@
+-- =====================================================
+-- fix_menu_components.sql 执行状态验证脚本
+-- 在租户库执行:SELECT component FROM sys_menu WHERE menu_id=29355;
+-- 预期返回: 'crm/customer/index'
+-- 如果返回 'admin/crm/customer/index' → fix未执行 → 311个动态菜单404
+-- =====================================================
+
+-- 1. 核心检查:menu_id=29355 (crm/customer)
+SELECT menu_id, menu_name, component,
+       CASE WHEN component = 'crm/customer/index' THEN 'OK' ELSE 'NEED_FIX' END AS status
+FROM sys_menu WHERE menu_id = 29355;
+
+-- 2. 批量检查:还有多少菜单的 component 仍以 admin/ 开头
+SELECT COUNT(*) AS needs_fix_count,
+       CASE WHEN COUNT(*) = 0 THEN '全部已修复' ELSE CONCAT('还有 ', COUNT(*), ' 条需要修复') END AS summary
+FROM sys_menu WHERE component LIKE 'admin/%';
+
+-- 3. 抽查几个关键菜单
+SELECT menu_id, menu_name, component FROM sys_menu WHERE menu_id IN (29194, 29384, 29525, 29543, 29556, 29639) ORDER BY menu_id;
+
+-- 4. 如果以上查询显示还有 admin/ 前缀,执行 fix_menu_components.sql
+-- 文件路径: d:\AICODE\saas\fix_menu_components.sql

+ 49 - 0
sql/hide_adminminui_placeholder_menus.sql

@@ -0,0 +1,49 @@
+-- =====================================================
+-- adminminui 菜单清理:隐藏不应出现在租户端的页面
+-- =====================================================
+-- 这些页面的功能归属"总后台(adminUI)"管理
+-- 在 adminminui 中要么是空占位、要么是假页面(无API调用)
+-- 应该在 adminminui 菜单中隐藏
+-- =====================================================
+-- 隐藏清单(共 7 个):
+--   类别 A:空占位页面(仅显示"请到总后台管理")
+--     1. admin/dailyStatistics/index  — 每日统计
+--     2. admin/proxy/feeConfig/index  — 代理收费配置
+--     3. admin/serviceCost/index      — 服务成本价配置
+--   类别 B:代理管理页面(归属 adminUI,adminminui 中无实际功能)
+--     4. proxy/servicePrice/index     — 服务价格
+--     5. proxy/tenant/index           — 代理租户
+--     6. proxy/tenantRel/index        — 代理租户关系
+--     7. proxy/withdraw/index         — 提现管理
+-- =====================================================
+-- 适用表:
+--   tenant_sys_menu  ← 新租户菜单模板(admin库)
+--   sys_menu         ← 已有租户的动态菜单(各租户库)
+--   fs_menu          ← adminUI 总后台自身菜单(如存在)
+-- =====================================================
+
+-- 1. 从 tenant_sys_menu 模板表删除(影响新租户创建)
+DELETE FROM tenant_sys_menu
+WHERE component IN (
+    -- 类别 A:空占位页面
+    'admin/dailyStatistics/index',
+    'admin/proxy/feeConfig/index',
+    'admin/serviceCost/index',
+    -- 类别 B:代理管理页面(归属 adminUI)
+    'proxy/servicePrice/index',
+    'proxy/tenant/index',
+    'proxy/tenantRel/index',
+    'proxy/withdraw/index'
+);
+
+-- 验证:执行后应返回 0 行
+SELECT COUNT(*) AS remaining FROM tenant_sys_menu
+WHERE component IN (
+    'admin/dailyStatistics/index',
+    'admin/proxy/feeConfig/index',
+    'admin/serviceCost/index',
+    'proxy/servicePrice/index',
+    'proxy/tenant/index',
+    'proxy/tenantRel/index',
+    'proxy/withdraw/index'
+);

+ 95 - 0
sql/lobster_menu_init.sql

@@ -0,0 +1,95 @@
+-- =====================================================
+-- 龙虾引擎 (Lobster Workflow Engine) 菜单初始化
+-- 将前端静态路由 /lobster/* 的 12 个子页面录入 sys_menu
+-- 租户管理员可通过角色权限控制访问
+--
+-- 【重要】租户菜单的三层体系:
+--   ┌─────────────────────────────────────────────────────┐
+--   │ 管理员数据库(ylrz_saas)                                │
+--   │   ├── fs_menu         ← adminUI 总后台自己的菜单       │
+--   │   │    (admin_menu_init.sql, add_missing_admin_menu.sql) │
+--   │   └── tenant_sys_menu ← 新租户菜单模板(创建时复制)    │
+--   └─────────────────────────────────────────────────────┘
+--   ┌─────────────────────────────────────────────────────┐
+--   │ 租户数据库(tenant_xxx)                                │
+--   │   └── sys_menu        ← 租户实际看到的动态菜单        │
+--   │   来源:                                              │
+--   │     • 新租户:TenantUtils.initMenus() 从 tenant_sys_menu 复制 │
+--   │     • 已有租户:TenantUpgradeService 执行 V*__*.sql 迁移脚本  │
+--   └─────────────────────────────────────────────────────┘
+--
+-- 用法:
+--   1. 已有租户:直接在各租户库执行本脚本,或通过迁移脚本自动应用
+--      (推荐使用迁移脚本:fs-admin-saas/.../V20260521_01__add_lobster_engine_menus.sql)
+--   2. 新租户:需确保 tenant_sys_menu 模板表也包含这些条目
+--      迁移脚本末尾已包含 INSERT INTO tenant_sys_menu 同步逻辑
+-- =====================================================
+
+-- 1. 根菜单:龙虾引擎 (parent_id=0 作为顶层菜单)
+INSERT 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 (29900, '龙虾引擎', 0, 100, '/lobster', '', 'M', 'el-icon-cpu', '0', '0', 0, 0, 'admin', NOW(), 'Lobster Workflow Engine顶层菜单');
+
+-- 2. AI生产工作流 目录(包含画布和模板库两个子页)
+INSERT 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 (29901, 'AI生产工作流', 29900, 1, 'production-workflow', '', 'M', 'el-icon-component', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 2a. 工作流画布
+INSERT 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 (29902, '工作流画布', 29901, 1, 'canvas', 'lobster/workflow-canvas/index', 'C', 'el-icon-chart', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 2b. 工作流模板库
+INSERT 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 (29903, '工作流模板库', 29901, 2, 'template', 'lobster/template/index', 'C', 'el-icon-documentation', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 3. AI生成工作流
+INSERT 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 (29904, 'AI生成工作流', 29900, 2, 'workflow-generate', 'lobster/workflow-generate/index', 'C', 'el-icon-build', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 4. 实例监控
+INSERT 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 (29905, '实例监控', 29900, 3, 'instance', 'lobster/instance/index', 'C', 'el-icon-monitor', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 5. AI优化建议
+INSERT 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 (29906, 'AI优化建议', 29900, 4, 'optimization', 'lobster/optimization/index', 'C', 'el-icon-eye-open', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 6. 提示词管理
+INSERT 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 (29907, '提示词管理', 29900, 5, 'prompt', 'lobster/prompt/index', 'C', 'el-icon-edit', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 7. 销冠语料学习
+INSERT 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 (29908, '销冠语料学习', 29900, 6, 'sales-corpus', 'lobster/sales-corpus/index', 'C', 'el-icon-star', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 8. 接口注册中心
+INSERT 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 (29909, '接口注册中心', 29900, 7, 'api-registry', 'lobster/api-registry/index', 'C', 'el-icon-nested', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 9. 死信队列
+INSERT 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 (29910, '死信队列', 29900, 8, 'dead-letter', 'lobster/dead-letter/index', 'C', 'el-icon-bug', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 10. 节点审核
+INSERT 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 (29911, '节点审核', 29900, 9, 'event-audit', 'lobster/event-audit/index', 'C', 'el-icon-checkbox', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 11. 聚合聊天
+INSERT 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 (29912, '聚合聊天', 29900, 10, 'chat-aggregate', 'lobster/chat-aggregate/index', 'C', 'el-icon-message', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 12. 模型配置
+INSERT 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 (29913, '模型配置', 29900, 11, 'model-config', 'lobster/model-config/index', 'C', 'el-icon-server', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- 13. Token系数管理
+INSERT 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 (29914, 'Token系数管理', 29900, 12, 'billing', 'lobster/billing/index', 'C', 'el-icon-money', '0', '0', 0, 0, 'admin', NOW(), NULL);
+
+-- =====================================================
+-- 将龙虾引擎菜单授权给租户默认角色(根据实际role_id调整)
+-- 假设租户端管理员角色role_key='tenant_admin',请先查询role_id
+-- SELECT role_id FROM sys_role WHERE role_key = 'tenant_admin';
+-- 然后将下面的 1 替换为实际role_id
+-- =====================================================
+-- INSERT INTO sys_role_menu (role_id, menu_id)
+-- SELECT 1, menu_id FROM sys_menu WHERE menu_id BETWEEN 29900 AND 29914;

+ 21 - 0
sql/update_admin_ai_model_menu.sql

@@ -0,0 +1,21 @@
+-- =====================================================
+-- adminUI 菜单更新:替换 textModel → aiModel
+-- 说明:
+--   1. 将旧的 "文本模型配置" (2611) 更新为新的 "AI模型配置" (aiModel)
+--   2. 新增 aiModel 的增删改查权限按钮(如需要)
+-- =====================================================
+
+-- 1. 更新 textModel(2611) → aiModel
+UPDATE fs_menu SET
+    menu_name = 'AI模型配置',
+    path = 'aiModel',
+    component = 'admin/aiModel/index',
+    perms = 'admin:aiModel:list',
+    icon = 'el-icon-cpu',
+    remark = '统一AI模型 + 场景配置',
+    update_time = NOW()
+WHERE menu_id = 2611;
+
+-- 2. 验证更新结果
+SELECT menu_id, menu_name, path, component, perms, icon 
+FROM fs_menu WHERE menu_id = 2611;