#AI
zhou zhou
5 天以前 51877df13075ad10ef51107f15bcd21f1661febe
#AI
2个文件已删除
96个文件已添加
2 文件已重命名
351个文件已修改
13759 ■■■■■ 已修改文件
docs/AI_DEVELOPMENT_GUIDE.md 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/ai/AiChatWidget.jsx 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/components/AiConsoleLayout.jsx 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiCallLog/AiCallLogEdit.jsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiCallLog/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosis/AiDiagnosisEdit.jsx 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosis/AiDiagnosisList.jsx 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosis/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosisPlan/AiDiagnosisPlanCreate.jsx 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosisPlan/AiDiagnosisPlanEdit.jsx 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosisPlan/AiDiagnosisPlanList.jsx 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiDiagnosisPlan/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountCreate.jsx 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountEdit.jsx 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx 618 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiParam/AiParamList.jsx 165 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiPrompt/AiPromptCreate.jsx 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiPrompt/AiPromptEdit.jsx 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx 383 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiPrompt/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiRoute/AiRouteCreate.jsx 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiRoute/AiRouteEdit.jsx 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiRoute/AiRouteList.jsx 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiRoute/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiToolConfig/AiToolConfigCreate.jsx 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiToolConfig/AiToolConfigEdit.jsx 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiToolConfig/AiToolConfigList.jsx 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiToolConfig/index.jsx 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/AiGatewayProperties.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/WebAsyncConfig.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/controller/AiGatewayController.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatRequest.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/AiGatewayService.java 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/GatewayStreamEvent.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-ai-gateway/src/main/resources/application.yml 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ServerBoot.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiProperties.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiSchemaGuard.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/constant/AiMcpConstants.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/constant/AiSceneCode.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiController.java 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionCreateRequest.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionRenameRequest.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatMessage.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatRequest.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/mapper/AiChatMessageMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/mapper/AiChatSessionMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatMessage.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatSession.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiDiagnosticToolResult.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiMcpToolDescriptor.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiGatewayClient.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiModelRouteRuntimeService.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextProvider.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptRuntimeService.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiRuntimeConfigService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiSessionService.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTextCompletionService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiChatStreamOrchestrator.java 467 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisMcpRuntimeService.java 426 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisPlanRunnerService.java 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisPlanScheduler.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisReportService.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisRuntimeService.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosticToolService.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiSessionServiceImpl.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpHttpClient.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpPayloadMapper.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpProtocolService.java 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpRegistryService.java 567 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpSseClient.java 434 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiApiFailureSummaryService.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiDeviceSiteSummaryService.java 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiDiagnosticDataProvider.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiOperationRecordSummaryService.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiTaskSummaryService.java 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiWarehouseSummaryService.java 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RemotesInfoProperties.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RestTemplateConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/WcsController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/BaseInfoController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/ErpQueryController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/ReportMsgController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/SyncOrderController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/BaseMatParms.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/BaseSyncParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/CheckObjParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/CompaniesParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryQueryConditionParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/LocAreasParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ManualShelvingParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OpStockParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OrderItem.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OrderParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OtherReceiptParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/PublicToStockParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/QueryOrderParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReceiptParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReportDataParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReportParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SaveCheckDiffParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncCheckDiffParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncLocReviseParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncLocsParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncMatGroupsParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncReviseItems.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncTransferItems.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncTransferParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/TaskInParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/WarehouseParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/dto/CheckDiffDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/dto/TransferInfoDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/dto/WkOrderDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/mcp/AiMcpProtocolController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/mcp/McpController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/mes/MesController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/AgvController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/MobileController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/MonitorController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaCheckOrderController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaOtherController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaOutStockController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/SysInfoController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/CommonResponse.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/constant/RcsConstant.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/CheckObjDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/ContainerWaveDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/InTaskMsgDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/InspectDetlDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/InspectItemDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/LocTypeDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/OperateStockDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/PoItemsDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/ReceiptDetlsDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/SyncLocsDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/TaskLocAreaDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/TaskQueueDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/enums/CallBackEvent.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/enums/MatnrDefectType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/enums/WcsMsgTypeEvent.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/CallForEmptyContainersParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ChangeLocParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/CommonRequest.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ContainerWaveParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/CreateInTaskParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ErpInspectItem.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ErpInspectParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ExMsgParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/InTaskWcsReportParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/MissionTaskIssueParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/OrderOutGeneralParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/PdaGeneralParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ReassignLocParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/TaskItemParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/TaskReportParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/WcsTaskParams.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/validator/SyncOrderValidator.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/AgvService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/MobileService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/MonitorService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/PdaCheckOrderService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/PdaOtherService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/PdaOutStockService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/ReceiveMsgService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/ReportMsgService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/WcsService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/AgvServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MonitorServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaCheckOrderServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOtherServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReportMsgServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/utils/LocUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/utils/SlaveProperties.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/utils/TimeConverterUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/annotation/OperationLog.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/aspect/OperationLogAspect.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/BeanConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/ConfigProperties.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/RedisProperties.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/SchedulerConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/SwaggerConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/SysStockProperties.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/WebMvcConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/config/WebSocketConfig.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/constant/Constants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/BaseParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/BusinessRes.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/KeyValVo.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/PageParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/PageResult.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/QueueTask.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/enums/WarehouseAreaType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/exception/BusinessException.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/handler/AggregationDataHandler.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/handler/ExcelDictHandlerImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/handler/global/GlobalDictService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/security/JwtAuthenticationFilter.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/security/JwtSubject.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/security/SecurityConfig.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/service/EmailService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/CommonUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/DataFieldSortFunc.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/DateUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/FileServerUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/Http.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/IpTools.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/JChardetFacadeUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/JSONUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/JwtUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/NodeUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/OpenOfficeUtil.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/CodeRes.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/DictTypeCode.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/GlobalConfigCode.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/SerialRuleCode.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiCallLogController.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiDiagnosisController.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiDiagnosisPlanController.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiDiagnosticToolConfigController.java 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiMcpMountController.java 394 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiParamController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiPromptController.java 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiRouteController.java 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/BaseController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/ConfigController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/DeptController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/DictDataController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/DictTypeController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FieldsController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FieldsItemController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowInstanceController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepInstanceController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepLogController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HostController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/MatnrRoleMenuController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/MenuController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/OperationRecordController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/PdaRoleMenuController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/RoleController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SerialRuleController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SerialRuleItemController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskInstanceController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskInstanceNodeController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateMergeController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TenantController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/UserController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/UserLoginController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/WarehouseRoleMenuController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/LoginParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/RegisterParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/RoleScopeParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/TenantInitParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/UpdatePasswordParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/result/LoginResult.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/result/MenuVo.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/result/SystemInfoVo.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiCallLog.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiDiagnosisPlan.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiDiagnosisRecord.java 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiDiagnosticToolConfig.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiMcpMount.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiModelRoute.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiParam.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiPromptPublishLog.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiPromptTemplate.java 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Config.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Dept.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/DictData.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/DictType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Fields.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FieldsItem.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowInstance.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowStepInstance.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowStepLog.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowStepTemplate.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Host.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/MatnrRoleMenu.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Menu.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/OperationRecord.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/PdaRoleMenu.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Role.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/RoleMenu.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/SerialRule.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/SerialRuleItem.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/SubsystemFlowTemplate.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskInstance.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskInstanceNode.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskPathTemplate.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskPathTemplateMerge.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskPathTemplateNode.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Tenant.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/User.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/UserLogin.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/UserRole.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/WarehouseRoleMenu.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/CompanyType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/ConfigType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/EmailType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/LoginSystemType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/SerialRuleReset.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/SerialRuleType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/StatusType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiCallLogMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiDiagnosisPlanMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiDiagnosisRecordMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiDiagnosticToolConfigMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiMcpMountMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiModelRouteMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiParamMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiPromptPublishLogMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiPromptTemplateMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/ConfigMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/DeptMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/DictDataMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/DictTypeMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FieldsItemMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FieldsMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowInstanceMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowStepInstanceMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowStepLogMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowStepTemplateMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/HostMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/MatnrRoleMenuMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/MenuMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/OperationRecordMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/PdaRoleMenuMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/RoleMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/RoleMenuMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/SerialRuleItemMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/SerialRuleMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/SubsystemFlowTemplateMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskInstanceMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskInstanceNodeMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskPathTemplateMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskPathTemplateMergeMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskPathTemplateNodeMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TenantMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/UserLoginMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/UserMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/UserRoleMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/WarehouseRoleMenuMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiCallLogService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiDiagnosisPlanService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiDiagnosisRecordService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiDiagnosticToolConfigService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiMcpMountService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiModelRouteService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiParamService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiPromptPublishLogService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiPromptTemplateService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/ConfigService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/DeptService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/DictDataService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/DictTypeService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FieldsItemService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FieldsService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowInstanceService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowStepInstanceService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowStepLogService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowStepTemplateService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/HostService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/MatnrRoleMenuService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/MenuService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/OperationRecordService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/PdaRoleMenuService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/RoleMenuService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/RoleService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/SerialRuleItemService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/SerialRuleService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/SubsystemFlowTemplateService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskInstanceNodeService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskInstanceService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskPathTemplateMergeService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskPathTemplateNodeService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskPathTemplateService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TenantService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/UserLoginService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/UserRoleService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/UserService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/WarehouseRoleMenuService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiCallLogServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiDiagnosisPlanServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiDiagnosisRecordServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiDiagnosticToolConfigServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiMcpMountServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiModelRouteServiceImpl.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiParamServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiPromptPublishLogServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiPromptTemplateServiceImpl.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/ConfigServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/DeptServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/DictDataServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/DictTypeServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FieldsItemServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FieldsServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowInstanceServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowStepInstanceServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowStepLogServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowStepTemplateServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/HostServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/MatnrRoleMenuServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/MenuServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/OperationRecordServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/PdaRoleMenuServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/RoleMenuServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/RoleServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/SerialRuleItemServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/SerialRuleServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/SubsystemFlowTemplateServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskInstanceNodeServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskInstanceServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskPathTemplateMergeServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskPathTemplateNodeServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskPathTemplateServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TenantServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/UserLoginServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/UserRoleServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/UserServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/WarehouseRoleMenuServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/utils/ExtendFieldsUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/utils/SerialRuleUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/utils/SystemAuthUtils.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/application.yml 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/20260311_ai_param.sql 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/20260311_ai_param_menu.sql 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/20260317_ai_all_in_one.sql 1194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/init.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/AI_DEVELOPMENT_GUIDE.md
New file
@@ -0,0 +1,186 @@
# WMS AI功能开发文档
## 1. 目标说明
本文档基于 `zy-wcs-master` 的 AI 巡检思路,对当前 `wms-master` 项目进行了功能映射和实现说明。
本次实现的目标不是照搬 Spring AI + MCP 的完整体系,而是复用当前项目已有的 `rsf-server + rsf-ai-gateway + rsf-admin` 架构,以最小改动补齐以下能力:
- 通用聊天场景与系统诊断场景并存
- 一键诊断入口
- 基于库存、任务、设备站点的实时诊断上下文
- AI会话与消息持久化
- 保持现有模型配置中心和网关调用链不变
## 2. 与 `zy-wcs-master` 的映射关系
| `zy-wcs-master` 能力 | 当前项目映射方案 |
| --- | --- |
| 一键巡检 / 连续追问 | `AiController` 新增诊断场景,前端增加“一键诊断” |
| Prompt 场景中心 | 使用 `sceneCode + AiProperties + AiPromptContextProvider` 组合实现 |
| MCP 工具聚合 | 使用现有上下文提供器直接查询业务表,先实现轻量级数据工具能力 |
| 会话落库 | 新增 `sys_ai_chat_session`、`sys_ai_chat_message` |
| LLM 网关 | 继续复用 `rsf-ai-gateway` |
说明:当前版本没有直接引入 `zy-wcs-master` 的 Prompt 管理后台、MCP 挂载中心、模型路由容灾中心。这些能力保留为下一阶段增强项。
## 3. 当前实现架构
### 3.1 模块分工
- `rsf-admin`
  - AI 对话抽屉组件
  - 新增一键诊断按钮
- `rsf-server`
  - AI控制器、场景路由、上下文拼装、会话存储
  - 直接从 WMS 业务表读取诊断摘要
- `rsf-ai-gateway`
  - 统一封装 OpenAI 兼容流式接口
### 3.2 调用链路
```mermaid
flowchart LR
    A["rsf-admin AI对话组件"] --> B["rsf-server /ai/chat/stream"]
    A --> C["rsf-server /ai/diagnose/stream"]
    B --> D["AiPromptContextService"]
    C --> D
    D --> E["库存摘要"]
    D --> F["任务摘要"]
    D --> G["设备站点摘要"]
    B --> H["AiSessionService"]
    C --> H
    B --> I["rsf-ai-gateway"]
    C --> I
    I --> J["OpenAI兼容模型接口"]
```
## 4. 后端实现说明
### 4.1 场景定义
新增场景编码:
- `general_chat`:普通对话
- `system_diagnose`:系统巡检诊断
相关位置:
- `rsf-server/src/main/java/com/vincent/rsf/server/ai/constant/AiSceneCode.java`
- `rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java`
- `rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java`
### 4.2 控制器能力
`AiController` 当前提供两条 SSE 能力:
- `/ai/chat/stream`
  - 通用聊天
  - 支持显式传入 `sceneCode`
- `/ai/diagnose/stream`
  - 诊断快捷入口
  - 自动使用 `system_diagnose`
  - 如果前端未传消息,则使用默认巡检提示词
### 4.3 Prompt 与上下文策略
当前实现采用“基础提示词 + 场景提示词 + 数据摘要提示词”的组合方式:
1. 基础提示词来自模型配置或 `application.yml`
2. 诊断场景追加 `diagnosis-system-prompt`
3. `AiPromptContextService` 自动拼接上下文提供器内容
上下文提供器如下:
- `AiDiagnosisPromptProvider`
  - 输出诊断流程和回答结构约束
- `AiWarehouseSummaryService`
  - 汇总 `man_loc`、`man_loc_item`
- `AiTaskSummaryService`
  - 汇总 `man_task`
- `AiDeviceSiteSummaryService`
  - 汇总 `man_device_site`
### 4.4 诊断数据来源
本次实现优先接入实时业务表,不额外引入独立知识库:
- 库存与库位:`man_loc`、`man_loc_item`
- 任务:`man_task`
- 设备站点:`man_device_site`
这使得 AI 更偏向“运行态诊断助手”,而不是通用知识问答机器人。
## 5. 会话持久化设计
### 5.1 表结构
新增脚本:`version/db/20260316_ai_chat_storage.sql`
涉及两张表:
- `sys_ai_chat_session`
- `sys_ai_chat_message`
### 5.2 存储策略
`AiSessionServiceImpl` 采用“数据库优先,内存兜底”模式:
- 如果检测到两张表存在,则自动启用数据库持久化
- 如果未执行迁移脚本,则继续使用原有内存缓存逻辑
这样可以降低发布风险,适合分阶段上线。
### 5.3 上线建议
建议在正式启用前先执行以下脚本:
- `version/db/20260311_ai_param.sql`
- `version/db/20260316_ai_chat_storage.sql`
脚本执行完成后重启 `rsf-server`,使其在启动阶段检测到会话表并切换到持久化模式。
## 6. 前端交互说明
`rsf-admin/src/ai/AiChatWidget.jsx` 已增加“一键诊断”入口,入口位置包括:
- 会话顶部工具区
- 空白态引导区
- 底部输入区状态栏
点击后会向 `/ai/diagnose/stream` 发起流式请求,并自动带上:
- `sessionId`
- `modelCode`
- `sceneCode=system_diagnose`
## 7. 配置项说明
`rsf-server/src/main/resources/application.yml`
关键配置:
- `ai.system-prompt`
- `ai.diagnosis-system-prompt`
- `ai.default-model-code`
- `ai.max-context-messages`
模型接入配置仍然优先从 AI 参数管理读取,`application.yml` 主要承担默认兜底配置。
## 8. 与 `zy-wcs-master` 的差距
当前版本已经具备“可用”的诊断能力,但与 `zy-wcs-master` 相比仍有以下差距:
- 尚未实现独立的 Prompt 配置后台
- 尚未实现标准 Spring AI MCP Server / MCP Mount 聚合
- 尚未实现多模型路由、失败切换、调用日志中心
- 当前诊断依赖数据库摘要,尚未纳入日志流、设备实时线程状态等更深层数据
## 9. 下一阶段建议
如果继续向 `zy-wcs-master` 靠近,建议按以下顺序演进:
1. 抽象统一的“AI数据工具层”,替代零散的上下文提供器
2. 新增 Prompt 模板表和发布机制
3. 给 `rsf-ai-gateway` 增加调用日志、失败切换、模型路由
4. 将日志、任务异常、接口失败率纳入诊断上下文
5. 增加独立的 AI 管理页,用于诊断记录、Prompt 调优和模型切换
rsf-admin/src/ai/AiChatWidget.jsx
@@ -32,6 +32,7 @@
const DRAWER_WIDTH = 720;
const SESSION_WIDTH = 220;
const DIAGNOSIS_MESSAGE = '请对当前WMS系统进行一次巡检诊断,结合库存、任务、设备站点数据识别异常并给出处理建议。';
const parseSseChunk = (chunk, onEvent) => {
    const blocks = chunk.split('\n\n');
@@ -269,14 +270,12 @@
        }
    };
    const handleSend = async () => {
        if (!draft.trim() || !activeSessionId || sending) {
    const streamChat = async ({ userContent, endpoint, sceneCode }) => {
        if (!userContent || !activeSessionId || sending) {
            return;
        }
        const userContent = draft.trim();
        const sessionId = activeSessionId;
        const modelCode = activeSession?.modelCode || models[0]?.code;
        setDraft('');
        setError('');
        setSending(true);
        setMessagesBySession((prev) => {
@@ -304,7 +303,7 @@
        let receivedDelta = false;
        try {
            const response = await fetch(`${PREFIX_BASE_URL}ai/chat/stream`, {
            const response = await fetch(`${PREFIX_BASE_URL}${endpoint}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
@@ -314,7 +313,8 @@
                body: JSON.stringify({
                    sessionId,
                    message: userContent,
                    modelCode
                    modelCode,
                    sceneCode
                }),
                signal: controller.signal
            });
@@ -392,6 +392,30 @@
            setSending(false);
            streamControllerRef.current = null;
        }
    };
    const handleSend = async () => {
        if (!draft.trim() || !activeSessionId || sending) {
            return;
        }
        const userContent = draft.trim();
        setDraft('');
        await streamChat({
            userContent,
            endpoint: 'ai/chat/stream',
            sceneCode: 'general_chat'
        });
    };
    const handleDiagnose = async () => {
        if (!activeSessionId || sending) {
            return;
        }
        await streamChat({
            userContent: DIAGNOSIS_MESSAGE,
            endpoint: 'ai/diagnose/stream',
            sceneCode: 'system_diagnose'
        });
    };
    const assistantReplyText = (messageList) => {
@@ -575,6 +599,15 @@
                                        </MenuItem>
                                    ))}
                                </Select>
                                <Button
                                    variant="outlined"
                                    size="small"
                                    disabled={sending || !activeSessionId}
                                    onClick={handleDiagnose}
                                    sx={{ borderRadius: 2, whiteSpace: 'nowrap' }}
                                >
                                    一键诊断
                                </Button>
                            </Stack>
                            <Divider />
                            <Box
@@ -609,8 +642,11 @@
                                            开始新的智能对话
                                        </Typography>
                                        <Typography variant="body2">
                                            可以直接提问仓储业务问题,或切换模型开始新的会话。
                                            可以直接提问仓储业务问题,或点击一键诊断快速巡检当前WMS状态。
                                        </Typography>
                                        <Button variant="outlined" onClick={handleDiagnose} disabled={sending || !activeSessionId}>
                                            一键诊断
                                        </Button>
                                    </Stack>
                                ) : (
                                    <Stack spacing={2}>
@@ -704,6 +740,15 @@
                                        label={activeSession?.modelCode || '未选择模型'}
                                        sx={{ bgcolor: 'rgba(25,118,210,0.08)', color: 'primary.main' }}
                                    />
                                    <Button
                                        variant="text"
                                        size="small"
                                        disabled={sending || !activeSessionId}
                                        onClick={handleDiagnose}
                                        sx={{ minWidth: 'auto', px: 1 }}
                                    >
                                        一键诊断
                                    </Button>
                                    <Typography variant="caption" color="text.secondary">
                                        `Enter` 发送,`Shift + Enter` 换行
                                    </Typography>
@@ -715,7 +760,7 @@
                                    maxRows={6}
                                    value={draft}
                                    onChange={(event) => setDraft(event.target.value)}
                                    placeholder="输入问题,支持多会话和模型切换"
                                    placeholder="输入问题,支持多会话、多模型和一键诊断"
                                    onKeyDown={(event) => {
                                        if (event.key === 'Enter' && !event.shiftKey) {
                                            event.preventDefault();
rsf-admin/src/i18n/en.js
@@ -151,6 +151,13 @@
        operation: 'Operation',
        config: 'Config',
        aiParam: 'AI Params',
        aiPrompt: 'AI Prompt',
        aiDiagnosis: 'AI Diagnosis',
        aiDiagnosisPlan: 'AI Diagnosis Plan',
        aiCallLog: 'AI Call Log',
        aiRoute: 'AI Route',
        aiToolConfig: 'AI Diagnostic Tool',
        aiMcpMount: 'AI MCP Mount',
        tenant: 'Tenant',
        userLogin: 'Token',
        customer: 'Customer',
rsf-admin/src/i18n/zh.js
@@ -152,6 +152,13 @@
        operation: '操作日志',
        config: '配置参数',
        aiParam: 'AI参数',
        aiPrompt: 'AI提示词',
        aiDiagnosis: 'AI诊断记录',
        aiDiagnosisPlan: 'AI巡检计划',
        aiCallLog: 'AI调用日志',
        aiRoute: 'AI模型路由',
        aiToolConfig: 'AI诊断工具',
        aiMcpMount: 'AI MCP挂载',
        tenant: '租户管理',
        userLogin: '登录日志',
        customer: '客户表',
rsf-admin/src/page/ResourceContent.js
@@ -7,6 +7,13 @@
import host from "./system/host";
import config from "./system/config";
import aiParam from "./system/aiParam";
import aiPrompt from "./system/aiPrompt";
import aiDiagnosis from "./system/aiDiagnosis";
import aiDiagnosisPlan from "./system/aiDiagnosisPlan";
import aiCallLog from "./system/aiCallLog";
import aiRoute from "./system/aiRoute";
import aiToolConfig from "./system/aiToolConfig";
import aiMcpMount from "./system/aiMcpMount";
import tenant from "./system/tenant";
import role from "./system/role";
import userLogin from "./system/userLogin";
@@ -80,6 +87,20 @@
      return config;
    case "aiParam":
      return aiParam;
    case "aiPrompt":
      return aiPrompt;
    case "aiDiagnosis":
      return aiDiagnosis;
    case "aiDiagnosisPlan":
      return aiDiagnosisPlan;
    case "aiCallLog":
      return aiCallLog;
    case "aiRoute":
      return aiRoute;
    case "aiToolConfig":
      return aiToolConfig;
    case "aiMcpMount":
      return aiMcpMount;
    case "tenant":
      return tenant;
    case "role":
rsf-admin/src/page/components/AiConsoleLayout.jsx
New file
@@ -0,0 +1,133 @@
import React from "react";
import { alpha } from '@mui/material/styles';
import { Box, Grid, Paper, Stack, Typography } from '@mui/material';
const pageShellSx = {
    mt: 0.5,
    width: '100%',
};
export const AiConsoleLayout = ({ title, subtitle, actions, stats, children }) => (
    <Box sx={pageShellSx}>
        <Paper
            elevation={0}
            sx={{
                borderRadius: 3,
                px: { xs: 2, md: 2.5 },
                py: 2,
                overflow: 'hidden',
                backgroundColor: '#fff',
                border: '1px solid #e6ebf2',
                boxShadow: '0 1px 3px rgba(15, 23, 42, 0.06)',
            }}
        >
            <Stack direction={{ xs: 'column', md: 'row' }} justifyContent="space-between" spacing={2}>
                <Box>
                    <Typography variant="h6" sx={{ fontWeight: 700, letterSpacing: 0.1 }}>
                        {title}
                    </Typography>
                    {subtitle ? (
                        <Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, maxWidth: 760 }}>
                            {subtitle}
                        </Typography>
                    ) : null}
                </Box>
                {actions ? (
                    <Stack direction="row" spacing={1} flexWrap="wrap" justifyContent="flex-end">
                        {actions}
                    </Stack>
                ) : null}
            </Stack>
            {stats?.length ? (
                <Grid container spacing={1.5} sx={{ mt: 1.5 }}>
                    {stats.map((item) => (
                        <Grid item xs={12} sm={6} md={12 / Math.min(stats.length, 4)} key={item.label}>
                            <Box
                                sx={{
                                    minHeight: 76,
                                    px: 1.5,
                                    py: 1.25,
                                    borderRadius: 2,
                                    border: '1px solid #e7ecf3',
                                    backgroundColor: '#fafbfd',
                                }}
                            >
                                <Typography variant="caption" color="text.secondary">
                                    {item.label}
                                </Typography>
                                <Typography variant="h5" sx={{ mt: 0.5, fontWeight: 700, lineHeight: 1.1 }}>
                                    {item.value}
                                </Typography>
                                {item.helper ? (
                                    <Typography variant="caption" color="text.secondary">
                                        {item.helper}
                                    </Typography>
                                ) : null}
                            </Box>
                        </Grid>
                    ))}
                </Grid>
            ) : null}
        </Paper>
        <Box sx={{ mt: 1.5 }}>
            {children}
        </Box>
    </Box>
);
export const AiConsolePanel = ({ title, subtitle, action, children, minHeight }) => (
    <Paper
        elevation={0}
        sx={{
            height: '100%',
            minHeight: minHeight || 240,
            borderRadius: 3,
            border: '1px solid #e6ebf2',
            backgroundColor: '#fff',
            boxShadow: '0 1px 3px rgba(15, 23, 42, 0.06)',
            overflow: 'hidden',
        }}
    >
        <Stack
            direction="row"
            justifyContent="space-between"
            alignItems="flex-start"
            spacing={1}
            sx={{
                px: 2,
                py: 1.5,
                borderBottom: '1px solid #e6edf5',
                backgroundColor: alpha('#fafbfc', 1),
            }}
        >
            <Box>
                <Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
                    {title}
                </Typography>
                {subtitle ? (
                    <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
                        {subtitle}
                    </Typography>
                ) : null}
            </Box>
            {action}
        </Stack>
        <Box sx={{ p: 1.5 }}>
            {children}
        </Box>
    </Paper>
);
export const aiCardSx = (active) => ({
    p: 1.5,
    borderRadius: 2.5,
    border: active ? '1px solid #d7e2ef' : '1px solid #e5eaf0',
    backgroundColor: '#fff',
    boxShadow: active
        ? '0 2px 8px rgba(15, 23, 42, 0.08)'
        : '0 1px 4px rgba(15, 23, 42, 0.04)',
    transition: 'box-shadow 0.18s ease, border-color 0.18s ease',
    '&:hover': {
        boxShadow: '0 3px 10px rgba(15, 23, 42, 0.08)',
    },
});
rsf-admin/src/page/system/aiCallLog/AiCallLogEdit.jsx
New file
@@ -0,0 +1,38 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
} from 'react-admin';
import { Stack, Grid, Typography } from '@mui/material';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
const AiCallLogEdit = () => (
    <Edit actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm toolbar={false}>
            <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12}>
                    <Typography variant="h6" gutterBottom>调用详情</Typography>
                    <Stack direction='row' gap={2}>
                        <TextInput source="routeCode" label="路由编码" disabled />
                        <TextInput source="modelCode" label="模型编码" disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <NumberInput source="attemptNo" label="尝试序号" disabled />
                        <TextInput source="result$" label="结果" disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <NumberInput source="spendTime" label="耗时(ms)" disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="err" label="错误信息" fullWidth multiline minRows={4} disabled />
                    </Stack>
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
);
export default AiCallLogEdit;
rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx
New file
@@ -0,0 +1,144 @@
import React, { useEffect, useState } from "react";
import {
    List,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextInput,
    DateInput,
    SelectInput,
    useListContext,
    Pagination,
} from 'react-admin';
import { Box, Chip, Grid, Typography } from '@mui/material';
import EmptyData from "@/page/components/EmptyData";
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
import MyExportButton from '@/page/components/MyExportButton';
import { DEFAULT_PAGE_SIZE } from '@/config/setting';
import request from "@/utils/request";
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <TextInput source="modelCode" label="模型编码" />,
    <TextInput source="routeCode" label="路由编码" />,
    <SelectInput source="result" label="结果" choices={[
        { id: '1', name: '成功' },
        { id: '0', name: '失败' },
    ]} />,
];
const resultColor = (result) => Number(result) === 1 ? 'success' : 'error';
const CallLogBoard = () => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const [stats, setStats] = useState({});
    useEffect(() => {
        let mounted = true;
        const fetchStats = async () => {
            try {
                const { data: res } = await request.get('/ai/call-log/stats');
                if (mounted && res?.code === 200) {
                    setStats(res.data || {});
                }
            } catch (error) {
            }
        };
        fetchStats();
        return () => {
            mounted = false;
        };
    }, []);
    if (!isLoading && !records.length) {
        return <EmptyData />;
    }
    return (
        <AiConsoleLayout
            title="AI调用日志"
            subtitle="按当前系统后台风格展示模型调用观测,突出成功率、最近 24 小时调用量和平均耗时,便于快速排查路由与上游模型问题。"
            stats={[
                { label: '调用总数', value: stats.total || 0 },
                { label: '成功率', value: `${Number(stats.successRate || 0).toFixed(1)}%` },
                { label: '平均耗时', value: `${stats.avgSpendTime || 0} ms` },
                { label: '近24小时', value: stats.last24hCount || 0 },
            ]}
        >
            <AiConsolePanel
                title="调用明细"
                subtitle={`已观测模型 ${stats.modelCount || 0} 个,路由 ${stats.routeCount || 0} 条。`}
                minHeight={420}
            >
                <Grid container spacing={2}>
                    {records.map((record) => (
                        <Grid item xs={12} md={6} xl={4} key={record.id}>
                            <Box sx={aiCardSx(record.result === 1)}>
                                <Box display="flex" justifyContent="space-between" gap={1}>
                                    <Box>
                                        <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                            {record.modelCode || '未记录模型'}
                                        </Typography>
                                        <Typography variant="caption" sx={{ color: '#8093a8' }}>
                                            {record.routeCode || '未记录路由'} · 第 {record.attemptNo || 1} 次
                                        </Typography>
                                    </Box>
                                    <Chip size="small" color={resultColor(record.result)} label={record.result$ || '未知'} />
                                </Box>
                                <Box sx={{ mt: 1.25, display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 1.5 }}>
                                    <Box>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>耗时</Typography>
                                        <Typography variant="body2" sx={{ color: '#31465d' }}>{record.spendTime || 0} ms</Typography>
                                    </Box>
                                    <Box>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>请求时间</Typography>
                                        <Typography variant="body2" sx={{ color: '#31465d' }}>{record.requestTime || '-'}</Typography>
                                    </Box>
                                </Box>
                                <Box sx={{ mt: 1.25 }}>
                                    <Typography variant="caption" sx={{ color: '#70839a' }}>错误信息</Typography>
                                    <Typography variant="body2" sx={{ mt: 0.5, minHeight: 72, color: '#31465d' }}>
                                        {record.err || '无'}
                                    </Typography>
                                </Box>
                                <Box sx={{ mt: 1.5 }}>
                                    <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
                                </Box>
                            </Box>
                        </Grid>
                    ))}
                </Grid>
                <Box sx={{ mt: 2 }}>
                    <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                </Box>
            </AiConsolePanel>
        </AiConsoleLayout>
    );
};
const AiCallLogList = () => (
    <List
        sx={{ width: '100%', flexGrow: 1 }}
        title={"menu.aiCallLog"}
        filters={filters}
        sort={{ field: "createTime", order: "desc" }}
        actions={(
            <TopToolbar>
                <FilterButton />
                <SelectColumnsButton preferenceKey='aiCallLog' />
                <MyExportButton />
            </TopToolbar>
        )}
        perPage={DEFAULT_PAGE_SIZE}
        pagination={false}
    >
        <CallLogBoard />
    </List>
);
export default AiCallLogList;
rsf-admin/src/page/system/aiCallLog/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiCallLogList from "./AiCallLogList";
import AiCallLogEdit from "./AiCallLogEdit";
export default {
    list: AiCallLogList,
    edit: AiCallLogEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.modelCode || record.id || ''}`
};
rsf-admin/src/page/system/aiDiagnosis/AiDiagnosisEdit.jsx
New file
@@ -0,0 +1,152 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
    useNotify,
    useRecordContext,
} from 'react-admin';
import { Box, Button, Chip, Grid, Paper, Stack, Typography } from '@mui/material';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
import request from "@/utils/request";
const ReportExportButton = () => {
    const record = useRecordContext();
    const notify = useNotify();
    if (!record?.id) {
        return null;
    }
    const handleExport = async () => {
        try {
            const res = await request.get(`/ai/diagnosis/report/export/${record.id}`, {
                responseType: 'blob',
            });
            const blob = new Blob([res.data], { type: 'text/markdown;charset=utf-8' });
            const link = document.createElement('a');
            link.href = window.URL.createObjectURL(blob);
            link.setAttribute('download', `${record.diagnosisNo || 'ai-diagnosis-report'}.md`);
            document.body.appendChild(link);
            link.click();
            link.remove();
        } catch (error) {
            notify(error.message || '导出失败', { type: 'error' });
        }
    };
    return <Button variant="outlined" onClick={handleExport}>导出报告</Button>;
};
const ToolTracePanel = () => {
    const record = useRecordContext();
    if (!record?.toolSummary) {
        return (
            <Typography variant="body2" color="text.secondary">
                暂无工具轨迹数据
            </Typography>
        );
    }
    let tools = [];
    try {
        const parsed = JSON.parse(record.toolSummary);
        if (Array.isArray(parsed)) {
            tools = parsed;
        }
    } catch (error) {
    }
    if (!tools.length) {
        return (
            <TextInput source="toolSummary" label="工具摘要(JSON)" fullWidth multiline minRows={6} disabled />
        );
    }
    return (
        <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: 2 }}>
            {tools.map((item, index) => (
                <Paper key={`${item.toolCode || 'tool'}-${index}`} variant="outlined" sx={{ p: 2, borderRadius: 2 }}>
                    <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                        <Box>
                            <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
                                {item.toolName || item.toolCode || `工具${index + 1}`}
                            </Typography>
                            <Typography variant="caption" color="text.secondary">
                                {(item.mcpToolName || item.toolCode || '-') + (item.mountCode ? ` · ${item.mountCode}` : '')}
                            </Typography>
                        </Box>
                        <Chip size="small" label={item.severity || 'INFO'} color={item.severity === 'ERROR' ? 'error' : (item.severity === 'WARN' ? 'warning' : 'primary')} />
                    </Stack>
                    <Typography variant="body2" sx={{ mt: 1.25, whiteSpace: 'pre-wrap' }}>
                        {item.summaryText || '无摘要'}
                    </Typography>
                </Paper>
            ))}
        </Box>
    );
};
const AiDiagnosisEdit = () => (
    <Edit actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm toolbar={false}>
            <Grid container width={{ xs: '100%', xl: '85%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12}>
                    <Typography variant="h6" gutterBottom>诊断概览</Typography>
                    <Stack direction='row' gap={2} flexWrap="wrap">
                        <TextInput source="diagnosisNo" label="诊断编号" disabled />
                        <TextInput source="sceneCode$" label="场景" disabled />
                        <TextInput source="modelCode" label="模型编码" disabled />
                        <TextInput source="result$" label="结果" disabled />
                        <NumberInput source="spendTime" label="耗时(ms)" disabled />
                    </Stack>
                    <Stack direction='row' gap={2} sx={{ mt: 1 }}>
                        <ReportExportButton />
                    </Stack>
                </Grid>
                <Grid item xs={12}>
                    <Typography variant="h6" gutterBottom>报告摘要</Typography>
                    <Stack direction='row' gap={2}>
                        <TextInput source="reportTitle" label="报告标题" fullWidth disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="question" label="诊断问题" fullWidth multiline minRows={3} disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="executiveSummary" label="问题概述" fullWidth multiline minRows={4} disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="evidenceSummary" label="关键证据" fullWidth multiline minRows={4} disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="actionSummary" label="建议动作" fullWidth multiline minRows={4} disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="riskSummary" label="风险评估" fullWidth multiline minRows={4} disabled />
                    </Stack>
                </Grid>
                <Grid item xs={12}>
                    <Typography variant="h6" gutterBottom>原始数据</Typography>
                    <Stack direction='row' gap={2}>
                        <TextInput source="conclusion" label="AI结论" fullWidth multiline minRows={6} disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="reportMarkdown" label="报告Markdown" fullWidth multiline minRows={8} disabled />
                    </Stack>
                    <Box sx={{ mt: 2 }}>
                        <Typography variant="subtitle1" gutterBottom>工具执行轨迹</Typography>
                        <ToolTracePanel />
                    </Box>
                    <Stack direction='row' gap={2}>
                        <TextInput source="err" label="错误信息" fullWidth multiline minRows={3} disabled />
                    </Stack>
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
);
export default AiDiagnosisEdit;
rsf-admin/src/page/system/aiDiagnosis/AiDiagnosisList.jsx
New file
@@ -0,0 +1,134 @@
import React from "react";
import {
    List,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextInput,
    DateInput,
    SelectInput,
    useListContext,
    Pagination,
} from 'react-admin';
import { Box, Chip, Grid, Stack, Typography } from '@mui/material';
import MyExportButton from '@/page/components/MyExportButton';
import { DEFAULT_PAGE_SIZE } from '@/config/setting';
import EmptyData from "@/page/components/EmptyData";
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <TextInput source="diagnosisNo" label="诊断编号" />,
    <TextInput source="modelCode" label="模型编码" />,
    <SelectInput source="result" label="结果" choices={[
        { id: '2', name: '运行中' },
        { id: '1', name: '成功' },
        { id: '0', name: '失败' },
    ]} />,
];
const resultColor = (result) => {
    if (result === 1) {
        return 'success';
    }
    if (result === 0) {
        return 'error';
    }
    return 'warning';
};
const DiagnosisBoard = () => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const successCount = records.filter((item) => item.result === 1).length;
    const failedCount = records.filter((item) => item.result === 0).length;
    const runningCount = records.filter((item) => item.result === 2).length;
    if (!isLoading && !records.length) {
        return <EmptyData />;
    }
    return (
        <AiConsoleLayout
            title="AI诊断报告"
            subtitle="诊断列表改成报告卡片视图,突出结论标题、处理结果和耗时;详情页继续承接完整报告与 Markdown 导出。"
            stats={[
                { label: '报告总数', value: records.length },
                { label: '成功', value: successCount },
                { label: '失败', value: failedCount },
                { label: '运行中', value: runningCount },
            ]}
        >
            <AiConsolePanel
                title="报告列表"
                subtitle="点击编辑进入报告详情页,可查看结构化报告、原始结论和导出 Markdown。"
                minHeight={460}
            >
                <Grid container spacing={2}>
                    {records.map((record) => (
                        <Grid item xs={12} md={6} xl={4} key={record.id}>
                            <Box sx={aiCardSx(record.result === 1)}>
                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                    <Box>
                                        <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                            {record.reportTitle || record.diagnosisNo}
                                        </Typography>
                                        <Typography variant="caption" sx={{ color: '#8093a8' }}>
                                            {record.sceneCode$} · {record.modelCode || '未记录模型'}
                                        </Typography>
                                    </Box>
                                    <Chip size="small" color={resultColor(record.result)} label={record.result$ || '未知'} />
                                </Stack>
                                <Typography variant="body2" sx={{ mt: 1.25, minHeight: 72, color: '#31465d' }}>
                                    {record.executiveSummary || record.question || '暂无诊断摘要'}
                                </Typography>
                                <Stack direction="row" spacing={2} sx={{ mt: 1.25 }}>
                                    <Box>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>耗时</Typography>
                                        <Typography variant="body2" sx={{ color: '#31465d' }}>{record.spendTime || 0} ms</Typography>
                                    </Box>
                                    <Box>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>开始时间</Typography>
                                        <Typography variant="body2" sx={{ color: '#31465d' }}>{record.startTime || '-'}</Typography>
                                    </Box>
                                </Stack>
                                <Box sx={{ mt: 1.5 }}>
                                    <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
                                </Box>
                            </Box>
                        </Grid>
                    ))}
                </Grid>
                <Box sx={{ mt: 2 }}>
                    <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                </Box>
            </AiConsolePanel>
        </AiConsoleLayout>
    );
};
const AiDiagnosisList = () => (
    <List
        sx={{ width: '100%', flexGrow: 1 }}
        title={"menu.aiDiagnosis"}
        filters={filters}
        sort={{ field: "createTime", order: "desc" }}
        actions={(
            <TopToolbar>
                <FilterButton />
                <SelectColumnsButton preferenceKey='aiDiagnosis' />
                <MyExportButton />
            </TopToolbar>
        )}
        perPage={DEFAULT_PAGE_SIZE}
        pagination={false}
    >
        <DiagnosisBoard />
    </List>
);
export default AiDiagnosisList;
rsf-admin/src/page/system/aiDiagnosis/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiDiagnosisList from "./AiDiagnosisList";
import AiDiagnosisEdit from "./AiDiagnosisEdit";
export default {
    list: AiDiagnosisList,
    edit: AiDiagnosisEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.diagnosisNo || record.id || ''}`
};
rsf-admin/src/page/system/aiDiagnosisPlan/AiDiagnosisPlanCreate.jsx
New file
@@ -0,0 +1,88 @@
import React from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    SaveButton,
    SelectInput,
    Toolbar,
    useNotify,
    Form,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
import StatusSelectInput from "@/page/components/StatusSelectInput";
import MemoInput from "@/page/components/MemoInput";
const sceneChoices = [
    { id: 'system_diagnose', name: '系统诊断' },
    { id: 'general_chat', name: '通用对话' },
];
const AiDiagnosisPlanCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async () => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <CreateBase
            record={{
                sceneCode: 'system_diagnose',
                cronExpr: '0 0/30 * * * ?',
                status: 1,
            }}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog open={open} onClose={handleClose} fullWidth disableRestoreFocus maxWidth="md">
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        {translate('create.title')}
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={6}><TextInput source="planName" label="计划名称" fullWidth /></Grid>
                            <Grid item xs={6}><SelectInput source="sceneCode" label="场景" choices={sceneChoices} fullWidth /></Grid>
                            <Grid item xs={6}><TextInput source="cronExpr" label="Cron表达式" fullWidth helperText="例如: 0 0/30 * * * ?" /></Grid>
                            <Grid item xs={6}><TextInput source="preferredModelCode" label="优先模型编码" fullWidth /></Grid>
                            <Grid item xs={12}><TextInput source="prompt" label="巡检提示词" fullWidth multiline minRows={5} /></Grid>
                            <Grid item xs={6}><StatusSelectInput fullWidth /></Grid>
                            <Grid item xs={12}><MemoInput /></Grid>
                        </Grid>
                    </DialogContent>
                    <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}>
                            <SaveButton />
                        </Toolbar>
                    </DialogActions>
                </Form>
            </Dialog>
        </CreateBase>
    )
}
export default AiDiagnosisPlanCreate;
rsf-admin/src/page/system/aiDiagnosisPlan/AiDiagnosisPlanEdit.jsx
New file
@@ -0,0 +1,69 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    SaveButton,
    SelectInput,
    Toolbar,
    DeleteButton,
} from 'react-admin';
import { Stack, Grid, Typography } from '@mui/material';
import { EDIT_MODE } from '@/config/setting';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
import MemoInput from "@/page/components/MemoInput";
import StatusSelectInput from "@/page/components/StatusSelectInput";
const sceneChoices = [
    { id: 'system_diagnose', name: '系统诊断' },
    { id: 'general_chat', name: '通用对话' },
];
const FormToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton mutationMode="optimistic" />
    </Toolbar>
);
const AiDiagnosisPlanEdit = () => (
    <Edit redirect="list" mutationMode={EDIT_MODE} actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm shouldUnregister warnWhenUnsavedChanges toolbar={<FormToolbar />} mode="onTouched">
            <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12} md={8}>
                    <Typography variant="h6" gutterBottom>主要</Typography>
                    <Stack direction='row' gap={2}>
                        <TextInput source="planName" label="计划名称" fullWidth />
                        <SelectInput source="sceneCode" label="场景" choices={sceneChoices} fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="cronExpr" label="Cron表达式" fullWidth />
                        <TextInput source="preferredModelCode" label="优先模型编码" fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="nextRunTime$" label="下次运行时间" disabled fullWidth />
                        <TextInput source="lastRunTime$" label="上次运行时间" disabled fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="lastResult$" label="最近结果" disabled fullWidth />
                        <TextInput source="lastDiagnosisId" label="最近诊断ID" disabled fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="lastMessage" label="最近消息" fullWidth multiline minRows={3} disabled />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="prompt" label="巡检提示词" fullWidth multiline minRows={6} />
                    </Stack>
                </Grid>
                <Grid item xs={12} md={4}>
                    <Typography variant="h6" gutterBottom>通用</Typography>
                    <StatusSelectInput />
                    <MemoInput />
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
)
export default AiDiagnosisPlanEdit;
rsf-admin/src/page/system/aiDiagnosisPlan/AiDiagnosisPlanList.jsx
New file
@@ -0,0 +1,175 @@
import React, { useState } from "react";
import {
    List,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextInput,
    DateInput,
    SelectInput,
    useListContext,
    Pagination,
    useNotify,
    useRefresh,
    DeleteButton,
} from 'react-admin';
import { Box, Button, Chip, Grid, Stack, Typography } from '@mui/material';
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import MyExportButton from '@/page/components/MyExportButton';
import { DEFAULT_PAGE_SIZE, OPERATE_MODE } from '@/config/setting';
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
import AiDiagnosisPlanCreate from "./AiDiagnosisPlanCreate";
import request from "@/utils/request";
const sceneChoices = [
    { id: 'system_diagnose', name: '系统诊断' },
    { id: 'general_chat', name: '通用对话' },
];
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <SelectInput source="sceneCode" label="场景" choices={sceneChoices} />,
    <TextInput source="planName" label="计划名称" />,
    <SelectInput source="status" label="状态" choices={[
        { id: '1', name: '启用' },
        { id: '0', name: '停用' },
    ]} />,
];
const RunPlanButton = ({ record }) => {
    const notify = useNotify();
    const refresh = useRefresh();
    const handleRun = async () => {
        try {
            const { data } = await request.post('/ai/diagnosis-plan/run', { id: record.id });
            if (data?.code !== 200) {
                throw new Error(data?.msg || '执行失败');
            }
            notify('已提交执行');
            refresh();
        } catch (error) {
            notify(error.message || '执行失败', { type: 'error' });
        }
    };
    return (
        <Button size="small" variant="outlined" onClick={handleRun} disabled={record.runningFlag === 1}>
            立即执行
        </Button>
    );
};
const PlanBoard = () => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const enabledCount = records.filter((item) => item.status === 1).length;
    const runningCount = records.filter((item) => item.runningFlag === 1).length;
    const successCount = records.filter((item) => item.lastResult === 1).length;
    if (!isLoading && !records.length) {
        return <EmptyData />;
    }
    return (
        <AiConsoleLayout
            title="AI巡检计划"
            subtitle="把一键诊断扩成可计划执行的巡检任务,按 Cron 定时触发,自动生成新的诊断记录和报告。"
            stats={[
                { label: '计划总数', value: records.length },
                { label: '启用', value: enabledCount },
                { label: '运行中', value: runningCount },
                { label: '最近成功', value: successCount },
            ]}
        >
            <AiConsolePanel
                title="计划列表"
                subtitle="计划执行时会自动写入诊断记录和调用日志;停用后不会继续触发,但仍可手动执行。"
                minHeight={420}
            >
                <Grid container spacing={2}>
                    {records.map((record) => (
                        <Grid item xs={12} md={6} xl={4} key={record.id}>
                            <Box sx={aiCardSx(record.status === 1)}>
                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                    <Box>
                                        <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                            {record.planName || '未命名计划'}
                                        </Typography>
                                        <Typography variant="caption" sx={{ color: '#8093a8' }}>
                                            {record.sceneCode$} · {record.cronExpr}
                                        </Typography>
                                    </Box>
                                    <Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
                                        <Chip size="small" color={record.status === 1 ? 'success' : 'default'} label={record.status === 1 ? '启用' : '停用'} />
                                        <Chip size="small" color={record.lastResult === 1 ? 'success' : (record.lastResult === 0 ? 'error' : 'warning')} label={record.lastResult$} />
                                    </Stack>
                                </Stack>
                                <Stack direction="row" spacing={2} sx={{ mt: 1.25 }}>
                                    <Box>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>优先模型</Typography>
                                        <Typography variant="body2" sx={{ color: '#31465d' }}>{record.preferredModelCode || '自动路由'}</Typography>
                                    </Box>
                                    <Box>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>下次运行</Typography>
                                        <Typography variant="body2" sx={{ color: '#31465d' }}>{record.nextRunTime$ || '-'}</Typography>
                                    </Box>
                                </Stack>
                                <Box sx={{ mt: 1.25 }}>
                                    <Typography variant="caption" sx={{ color: '#70839a' }}>最近消息</Typography>
                                    <Typography variant="body2" sx={{ mt: 0.5, minHeight: 72, color: '#31465d' }}>
                                        {record.lastMessage || record.prompt || '暂无执行记录'}
                                    </Typography>
                                </Box>
                                <Stack direction="row" spacing={1} sx={{ mt: 1.5 }} flexWrap="wrap">
                                    <RunPlanButton record={record} />
                                    <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
                                    <DeleteButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} mutationMode={OPERATE_MODE} />
                                </Stack>
                            </Box>
                        </Grid>
                    ))}
                </Grid>
                <Box sx={{ mt: 2 }}>
                    <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                </Box>
            </AiConsolePanel>
        </AiConsoleLayout>
    );
};
const AiDiagnosisPlanList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <Box display="flex" sx={{ width: '100%' }}>
            <List
                sx={{ width: '100%', flexGrow: 1 }}
                title={"menu.aiDiagnosisPlan"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
                sort={{ field: "nextRunTime", order: "asc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='aiDiagnosisPlan' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
                pagination={false}
            >
                <PlanBoard />
            </List>
            <AiDiagnosisPlanCreate open={createDialog} setOpen={setCreateDialog} />
        </Box>
    )
}
export default AiDiagnosisPlanList;
rsf-admin/src/page/system/aiDiagnosisPlan/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiDiagnosisPlanList from "./AiDiagnosisPlanList";
import AiDiagnosisPlanEdit from "./AiDiagnosisPlanEdit";
export default {
    list: AiDiagnosisPlanList,
    edit: AiDiagnosisPlanEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.planName || record.cronExpr || ''}`
};
rsf-admin/src/page/system/aiMcpMount/AiMcpMountCreate.jsx
New file
@@ -0,0 +1,92 @@
import React from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    useNotify,
    Form,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
import StatusSelectInput from "@/page/components/StatusSelectInput";
import MemoInput from "@/page/components/MemoInput";
const transportChoices = [
    { id: 'INTERNAL', name: '内部工具集' },
    { id: 'HTTP', name: 'Streamable HTTP' },
    { id: 'SSE', name: 'SSE MCP' },
];
const enabledChoices = [
    { id: 1, name: '启用' },
    { id: 0, name: '停用' },
];
const AiMcpMountCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async () => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <CreateBase
            record={{ transportType: 'INTERNAL', enabledFlag: 1, timeoutMs: 10000, status: 1 }}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog open={open} onClose={handleClose} fullWidth disableRestoreFocus maxWidth="md">
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        {translate('create.title')}
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={6}><TextInput source="name" label="挂载名称" fullWidth /></Grid>
                            <Grid item xs={6}><TextInput source="mountCode" label="挂载编码" fullWidth /></Grid>
                            <Grid item xs={6}><SelectInput source="transportType" label="传输类型" choices={transportChoices} fullWidth /></Grid>
                            <Grid item xs={6}><SelectInput source="enabledFlag" label="启用" choices={enabledChoices} fullWidth /></Grid>
                            <Grid item xs={6}><NumberInput source="timeoutMs" label="超时毫秒" fullWidth /></Grid>
                            <Grid item xs={6}><StatusSelectInput fullWidth /></Grid>
                            <Grid item xs={12}><TextInput source="url" label="地址" fullWidth helperText="内部工具集会自动写入 /ai/mcp;外部挂载可填写远程 Streamable HTTP 或 SSE MCP 地址" /></Grid>
                            <Grid item xs={12}><MemoInput /></Grid>
                        </Grid>
                    </DialogContent>
                    <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}>
                            <SaveButton />
                        </Toolbar>
                    </DialogActions>
                </Form>
            </Dialog>
        </CreateBase>
    )
}
export default AiMcpMountCreate;
rsf-admin/src/page/system/aiMcpMount/AiMcpMountEdit.jsx
New file
@@ -0,0 +1,74 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    DeleteButton,
} from 'react-admin';
import { Stack, Grid, Typography } from '@mui/material';
import { EDIT_MODE } from '@/config/setting';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
import MemoInput from "@/page/components/MemoInput";
import StatusSelectInput from "@/page/components/StatusSelectInput";
const transportChoices = [
    { id: 'INTERNAL', name: '内部工具集' },
    { id: 'HTTP', name: 'Streamable HTTP' },
    { id: 'SSE', name: 'SSE MCP' },
];
const enabledChoices = [
    { id: 1, name: '启用' },
    { id: 0, name: '停用' },
];
const FormToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton mutationMode="optimistic" />
    </Toolbar>
);
const AiMcpMountEdit = () => (
    <Edit redirect="list" mutationMode={EDIT_MODE} actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm shouldUnregister warnWhenUnsavedChanges toolbar={<FormToolbar />} mode="onTouched">
            <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12} md={8}>
                    <Typography variant="h6" gutterBottom>主要</Typography>
                    <Stack direction='row' gap={2}>
                        <TextInput source="name" label="挂载名称" fullWidth />
                        <TextInput source="mountCode" label="挂载编码" fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <SelectInput source="transportType" label="传输类型" choices={transportChoices} fullWidth />
                        <SelectInput source="enabledFlag" label="启用" choices={enabledChoices} fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="url" label="地址" fullWidth />
                        <NumberInput source="timeoutMs" label="超时毫秒" fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="lastTestResult$" label="最近测试结果" disabled fullWidth />
                        <TextInput source="lastTestTime$" label="最近测试时间" disabled fullWidth />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="lastToolCount" label="最近工具数" disabled fullWidth />
                        <TextInput source="lastTestMessage" label="最近测试消息" disabled fullWidth multiline minRows={2} />
                    </Stack>
                </Grid>
                <Grid item xs={12} md={4}>
                    <Typography variant="h6" gutterBottom>通用</Typography>
                    <StatusSelectInput />
                    <MemoInput />
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
)
export default AiMcpMountEdit;
rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
New file
@@ -0,0 +1,618 @@
import React, { useEffect, useMemo, useState } from "react";
import {
    Accordion,
    AccordionDetails,
    AccordionSummary,
    Alert,
    Box,
    Button,
    Chip,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    FormControl,
    Grid,
    InputLabel,
    MenuItem,
    Paper,
    Select,
    Stack,
    Switch,
    TextField,
    Typography,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import AddIcon from "@mui/icons-material/Add";
import EditIcon from "@mui/icons-material/Edit";
import StorageIcon from "@mui/icons-material/Storage";
import HubIcon from "@mui/icons-material/Hub";
import BuildIcon from "@mui/icons-material/Build";
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import { useNotify } from "react-admin";
import request from "@/utils/request";
import EmptyData from "@/page/components/EmptyData";
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
const usageChoices = [
    { id: "DIAGNOSE_ONLY", name: "仅诊断使用" },
    { id: "CHAT_AND_DIAGNOSE", name: "聊天与诊断都可用" },
    { id: "DISABLED", name: "禁用" },
];
const transportChoices = [
    { id: "AUTO", name: "自动识别" },
    { id: "HTTP", name: "Streamable HTTP" },
    { id: "SSE", name: "SSE" },
];
const authChoices = [
    { id: "NONE", name: "无认证" },
    { id: "BEARER", name: "Bearer Token" },
    { id: "API_KEY", name: "X-API-Key" },
];
const defaultServiceForm = {
    id: null,
    name: "",
    url: "",
    transportType: "AUTO",
    authType: "NONE",
    authValue: "",
    usageScope: "DIAGNOSE_ONLY",
    timeoutMs: 10000,
    enabledFlag: 1,
    memo: "",
};
const usageLabel = (usageScope) => {
    const matched = usageChoices.find((item) => item.id === usageScope);
    return matched ? matched.name : "仅诊断使用";
};
const transportLabel = (transportType) => {
    const matched = transportChoices.find((item) => item.id === transportType);
    return matched ? matched.name : transportType || "自动识别";
};
const BuiltInToolCard = ({ tool, onSave }) => {
    const notify = useNotify();
    const [usageScope, setUsageScope] = useState(tool.usageScope || "DIAGNOSE_ONLY");
    const [priority, setPriority] = useState(tool.priority || 10);
    const [toolPrompt, setToolPrompt] = useState(tool.toolPrompt || "");
    const [saving, setSaving] = useState(false);
    useEffect(() => {
        setUsageScope(tool.usageScope || "DIAGNOSE_ONLY");
        setPriority(tool.priority || 10);
        setToolPrompt(tool.toolPrompt || "");
    }, [tool]);
    const handleSave = async () => {
        try {
            setSaving(true);
            const { data: res } = await request.post("/ai/mcp/console/builtin-tool/save", {
                toolCode: tool.toolCode,
                toolName: tool.toolName,
                priority: priority || 10,
                toolPrompt,
                usageScope,
            });
            if (res?.code !== 200) {
                throw new Error(res?.msg || "保存失败");
            }
            notify("内置工具策略已更新");
            onSave?.(res?.data || []);
        } catch (error) {
            notify(error.message || "保存失败", { type: "error" });
        } finally {
            setSaving(false);
        }
    };
    const enabled = usageScope !== "DISABLED";
    return (
        <Box sx={aiCardSx(enabled)}>
            <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                <Box>
                    <Typography variant="subtitle1" sx={{ fontWeight: 700, color: "#284059" }}>
                        {tool.toolName}
                    </Typography>
                    <Typography variant="caption" sx={{ color: "#8093a8" }}>
                        {tool.toolCode}
                    </Typography>
                </Box>
                <Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
                    <Chip size="small" color={enabled ? "success" : "default"} label={enabled ? "启用" : "停用"} />
                    <Chip size="small" color="primary" label={usageLabel(usageScope)} />
                </Stack>
            </Stack>
            <Typography variant="body2" sx={{ mt: 1.5, minHeight: 44, color: "#31465d" }}>
                {tool.description || "系统内置工具,默认由平台托管。"}
            </Typography>
            <FormControl size="small" fullWidth sx={{ mt: 1.5 }}>
                <InputLabel>用途预设</InputLabel>
                <Select value={usageScope} label="用途预设" onChange={(event) => setUsageScope(event.target.value)}>
                    {usageChoices.map((item) => (
                        <MenuItem key={item.id} value={item.id}>{item.name}</MenuItem>
                    ))}
                </Select>
            </FormControl>
            <Accordion elevation={0} disableGutters sx={{ mt: 1.5, borderRadius: 2, border: "1px solid #dbe5f1" }}>
                <AccordionSummary expandIcon={<ExpandMoreIcon />}>
                    <Typography variant="body2" sx={{ fontWeight: 600 }}>高级设置</Typography>
                </AccordionSummary>
                <AccordionDetails>
                    <Stack spacing={1.5}>
                        <TextField
                            label="执行优先级"
                            size="small"
                            type="number"
                            value={priority}
                            onChange={(event) => setPriority(Number(event.target.value || 10))}
                        />
                        <TextField
                            label="附加规则"
                            size="small"
                            value={toolPrompt}
                            multiline
                            minRows={3}
                            onChange={(event) => setToolPrompt(event.target.value)}
                            helperText="这里只在需要覆盖默认工具说明时填写。"
                        />
                    </Stack>
                </AccordionDetails>
            </Accordion>
            <Stack direction="row" justifyContent="flex-end" sx={{ mt: 1.5 }}>
                <Button variant="contained" size="small" onClick={handleSave} disabled={saving}>
                    {saving ? "保存中..." : "保存"}
                </Button>
            </Stack>
        </Box>
    );
};
const ServiceDialog = ({ open, record, onClose, onSaved }) => {
    const notify = useNotify();
    const [form, setForm] = useState(defaultServiceForm);
    const [saving, setSaving] = useState(false);
    useEffect(() => {
        if (open) {
            setForm(record ? {
                ...defaultServiceForm,
                ...record,
                authType: record.authType || "NONE",
                transportType: record.transportType || "AUTO",
                usageScope: record.usageScope || "DIAGNOSE_ONLY",
                timeoutMs: record.timeoutMs || 10000,
            } : defaultServiceForm);
        }
    }, [open, record]);
    const handleChange = (field, value) => {
        setForm((prev) => ({ ...prev, [field]: value }));
    };
    const handleSave = async () => {
        try {
            setSaving(true);
            const payload = { ...form };
            if (payload.authType === "NONE") {
                payload.authValue = "";
            }
            const { data: res } = await request.post("/ai/mcp/console/service/save", payload);
            if (res?.code !== 200) {
                throw new Error(res?.msg || "保存失败");
            }
            notify("外部 MCP 服务已保存");
            onSaved?.(res?.data);
        } catch (error) {
            notify(error.message || "保存失败", { type: "error" });
        } finally {
            setSaving(false);
        }
    };
    return (
        <Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
            <DialogTitle>{form.id ? "编辑外部 MCP 服务" : "新增外部 MCP 服务"}</DialogTitle>
            <DialogContent dividers>
                <Grid container spacing={2} sx={{ mt: 0.25 }}>
                    <Grid item xs={12} md={6}>
                        <TextField label="服务名称" fullWidth size="small" value={form.name} onChange={(event) => handleChange("name", event.target.value)} />
                    </Grid>
                    <Grid item xs={12} md={6}>
                        <FormControl fullWidth size="small">
                            <InputLabel>连接方式</InputLabel>
                            <Select value={form.transportType} label="连接方式" onChange={(event) => handleChange("transportType", event.target.value)}>
                                {transportChoices.map((item) => (
                                    <MenuItem key={item.id} value={item.id}>{item.name}</MenuItem>
                                ))}
                            </Select>
                        </FormControl>
                    </Grid>
                    <Grid item xs={12}>
                        <TextField
                            label="服务地址"
                            fullWidth
                            size="small"
                            value={form.url}
                            onChange={(event) => handleChange("url", event.target.value)}
                            helperText="直接填写远程 MCP 服务地址;系统会优先按自动识别协议连接。"
                        />
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <FormControl fullWidth size="small">
                            <InputLabel>认证方式</InputLabel>
                            <Select value={form.authType} label="认证方式" onChange={(event) => handleChange("authType", event.target.value)}>
                                {authChoices.map((item) => (
                                    <MenuItem key={item.id} value={item.id}>{item.name}</MenuItem>
                                ))}
                            </Select>
                        </FormControl>
                    </Grid>
                    <Grid item xs={12} md={8}>
                        <TextField
                            label="认证信息"
                            fullWidth
                            size="small"
                            type={form.authType === "NONE" ? "text" : "password"}
                            value={form.authValue}
                            disabled={form.authType === "NONE"}
                            onChange={(event) => handleChange("authValue", event.target.value)}
                        />
                    </Grid>
                    <Grid item xs={12} md={6}>
                        <FormControl fullWidth size="small">
                            <InputLabel>用途预设</InputLabel>
                            <Select value={form.usageScope} label="用途预设" onChange={(event) => handleChange("usageScope", event.target.value)}>
                                {usageChoices.filter((item) => item.id !== "DISABLED").map((item) => (
                                    <MenuItem key={item.id} value={item.id}>{item.name}</MenuItem>
                                ))}
                            </Select>
                        </FormControl>
                    </Grid>
                    <Grid item xs={12} md={3}>
                        <TextField
                            label="超时毫秒"
                            type="number"
                            size="small"
                            fullWidth
                            value={form.timeoutMs}
                            onChange={(event) => handleChange("timeoutMs", Number(event.target.value || 10000))}
                        />
                    </Grid>
                    <Grid item xs={12} md={3}>
                        <Stack direction="row" alignItems="center" spacing={1} sx={{ height: "100%" }}>
                            <Switch checked={Number(form.enabledFlag) === 1} onChange={(event) => handleChange("enabledFlag", event.target.checked ? 1 : 0)} />
                            <Typography variant="body2">启用服务</Typography>
                        </Stack>
                    </Grid>
                    <Grid item xs={12}>
                        <TextField
                            label="备注"
                            fullWidth
                            size="small"
                            value={form.memo}
                            multiline
                            minRows={2}
                            onChange={(event) => handleChange("memo", event.target.value)}
                        />
                    </Grid>
                </Grid>
            </DialogContent>
            <DialogActions>
                <Button onClick={onClose}>取消</Button>
                <Button variant="contained" onClick={handleSave} disabled={saving}>{saving ? "保存中..." : "保存"}</Button>
            </DialogActions>
        </Dialog>
    );
};
const ExternalServiceCard = ({ service, onEdit, onTest, onInspect, onRemove }) => (
    <Box sx={aiCardSx(service.enabledFlag === 1)}>
        <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
            <Box>
                <Typography variant="subtitle1" sx={{ fontWeight: 700, color: "#284059" }}>
                    {service.name}
                </Typography>
                <Typography variant="caption" sx={{ color: "#8093a8" }}>
                    {transportLabel(service.transportType)} · {usageLabel(service.usageScope)}
                </Typography>
            </Box>
            <Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
                <Chip size="small" color={service.enabledFlag === 1 ? "success" : "default"} label={service.enabledFlag === 1 ? "启用" : "停用"} />
                <Chip size="small" color={service.lastTestResult === 1 ? "success" : "default"} label={service.lastTestResult$ || "未测试"} />
            </Stack>
        </Stack>
        <Typography variant="body2" sx={{ mt: 1.25, color: "#31465d", minHeight: 42 }}>
            {service.url}
        </Typography>
        {service.lastTestMessage ? (
            <Alert severity={service.lastTestResult === 1 ? "success" : "warning"} sx={{ mt: 1.25 }}>
                {service.lastTestMessage}
            </Alert>
        ) : null}
        <Typography variant="caption" display="block" sx={{ mt: 1.25, color: "#70839a" }}>
            {service.lastTestTime ? `最近测试: ${service.lastTestTime}` : "最近测试: -"}
        </Typography>
        <Typography variant="caption" display="block" sx={{ color: "#70839a" }}>
            {service.lastToolCount !== null && service.lastToolCount !== undefined ? `已发现工具: ${service.lastToolCount}` : "已发现工具: -"}
        </Typography>
        <Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1.5 }}>
            <Button size="small" variant="outlined" onClick={() => onInspect(service)}>查看工具</Button>
            <Button size="small" variant="outlined" onClick={() => onTest(service)}>连接测试</Button>
            <Button size="small" startIcon={<EditIcon />} onClick={() => onEdit(service)}>编辑</Button>
            <Button size="small" color="error" startIcon={<DeleteOutlineIcon />} onClick={() => onRemove(service)}>删除</Button>
        </Stack>
    </Box>
);
const ExternalToolsPanel = ({ selectedService, tools, preview, onPreview }) => (
    <Grid container spacing={2}>
        <Grid item xs={12} lg={7}>
            <AiConsolePanel
                title="外部服务工具目录"
                subtitle={selectedService ? `当前服务:${selectedService.name}` : "选中任一外部服务后,会展示最新发现的工具目录。"}
                minHeight={320}
            >
                <Box sx={{ display: "grid", gap: 1.5 }}>
                    {tools.map((tool) => (
                        <Paper key={tool.mcpToolName} variant="outlined" sx={{ p: 1.5, borderRadius: 2 }}>
                            <Stack direction="row" justifyContent="space-between" spacing={1}>
                                <Box>
                                    <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
                                        {tool.toolName || tool.mcpToolName}
                                    </Typography>
                                    <Typography variant="caption" color="text.secondary">
                                        {tool.mcpToolName}
                                    </Typography>
                                </Box>
                                <Button size="small" variant="text" onClick={() => onPreview(tool)}>执行预览</Button>
                            </Stack>
                            <Typography variant="body2" sx={{ mt: 1 }}>
                                {tool.description || tool.toolPrompt || "暂无说明"}
                            </Typography>
                        </Paper>
                    ))}
                    {!tools.length ? <EmptyData /> : null}
                </Box>
            </AiConsolePanel>
        </Grid>
        <Grid item xs={12} lg={5}>
            <AiConsolePanel
                title="工具预览"
                subtitle="用于快速确认远程工具是否真的能返回结果。"
                minHeight={320}
            >
                {preview ? (
                    <Box>
                        <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
                            {preview.toolName || preview.mcpToolName || preview.toolCode}
                        </Typography>
                        <Typography variant="caption" color="text.secondary">
                            {preview.severity || "INFO"}
                        </Typography>
                        <Typography variant="body2" sx={{ mt: 1.5, whiteSpace: "pre-wrap" }}>
                            {preview.summaryText || "暂无摘要"}
                        </Typography>
                    </Box>
                ) : (
                    <EmptyData />
                )}
            </AiConsolePanel>
        </Grid>
    </Grid>
);
const AiMcpMountList = () => {
    const notify = useNotify();
    const [overview, setOverview] = useState({ builtInMount: null, builtInTools: [], externalServices: [] });
    const [serviceDialogOpen, setServiceDialogOpen] = useState(false);
    const [editingService, setEditingService] = useState(null);
    const [selectedService, setSelectedService] = useState(null);
    const [serviceTools, setServiceTools] = useState([]);
    const [preview, setPreview] = useState(null);
    const loadOverview = async () => {
        try {
            const { data: res } = await request.get("/ai/mcp/console/overview");
            if (res?.code !== 200) {
                throw new Error(res?.msg || "加载失败");
            }
            setOverview(res.data || { builtInMount: null, builtInTools: [], externalServices: [] });
        } catch (error) {
            notify(error.message || "加载失败", { type: "error" });
        }
    };
    useEffect(() => {
        loadOverview();
    }, []);
    const stats = useMemo(() => {
        const builtInEnabled = (overview.builtInTools || []).filter((item) => item.usageScope !== "DISABLED").length;
        const externalEnabled = (overview.externalServices || []).filter((item) => item.enabledFlag === 1).length;
        const successCount = (overview.externalServices || []).filter((item) => item.lastTestResult === 1).length;
        const discoveredTools = (overview.externalServices || []).reduce((sum, item) => sum + (item.lastToolCount || 0), 0);
        return [
            { label: "内置工具启用", value: builtInEnabled },
            { label: "外部服务", value: (overview.externalServices || []).length },
            { label: "最近测试成功", value: successCount },
            { label: "发现外部工具", value: discoveredTools },
        ];
    }, [overview]);
    const handleBuiltInSaved = (tools) => {
        setOverview((prev) => ({ ...prev, builtInTools: tools }));
    };
    const handleServiceSaved = async () => {
        setServiceDialogOpen(false);
        setEditingService(null);
        await loadOverview();
    };
    const handleTestService = async (service) => {
        try {
            const { data: res } = await request.post("/ai/mcp/console/service/test", { id: service.id }, { timeout: (service.timeoutMs || 10000) + 5000 });
            if (res?.code !== 200) {
                throw new Error(res?.msg || "测试失败");
            }
            notify(res?.data?.message || "连接测试完成");
            await loadOverview();
            setSelectedService(res?.data?.service || service);
            setServiceTools(res?.data?.tools || []);
            setPreview(null);
        } catch (error) {
            notify(error.message || "测试失败", { type: "error" });
        }
    };
    const handleInspectService = async (service) => {
        try {
            const { data: res } = await request.get("/ai/mcp/mount/toolList", { params: { mountId: service.id } });
            if (res?.code !== 200) {
                throw new Error(res?.msg || "加载工具失败");
            }
            setSelectedService(service);
            setServiceTools(res.data || []);
            setPreview(null);
        } catch (error) {
            notify(error.message || "加载工具失败", { type: "error" });
        }
    };
    const handlePreviewTool = async (tool) => {
        try {
            const { data: res } = await request.post("/ai/mcp/mount/toolPreview", {
                mountCode: tool.mountCode,
                toolCode: tool.toolCode,
                sceneCode: tool.sceneCode,
                question: "请返回该工具当前的摘要预览结果",
            });
            if (res?.code !== 200) {
                throw new Error(res?.msg || "预览失败");
            }
            setPreview(res.data);
        } catch (error) {
            notify(error.message || "预览失败", { type: "error" });
        }
    };
    const handleRemoveService = async (service) => {
        try {
            const { data: res } = await request.post(`/ai/mcp/console/service/remove/${service.id}`);
            if (res?.code !== 200) {
                throw new Error(res?.msg || "删除失败");
            }
            notify("服务已删除");
            if (selectedService?.id === service.id) {
                setSelectedService(null);
                setServiceTools([]);
                setPreview(null);
            }
            await loadOverview();
        } catch (error) {
            notify(error.message || "删除失败", { type: "error" });
        }
    };
    return (
        <Box sx={{ width: "100%" }}>
            <AiConsoleLayout
                title="MCP中心"
                subtitle="内置工具由系统自动托管;外部 MCP 服务只保留接入地址、认证和用途预设,复杂协议细节由系统自动处理。"
                stats={stats}
            >
                <AiConsolePanel
                    title="内置工具"
                    subtitle="这些工具直接连接当前 WMS 内部数据,默认开箱即用,不需要手动配置挂载地址或协议。"
                    action={(
                        <Chip
                            icon={<StorageIcon />}
                            label={overview.builtInMount ? "系统托管" : "待初始化"}
                            color={overview.builtInMount ? "success" : "default"}
                            variant="outlined"
                        />
                    )}
                    minHeight={260}
                >
                    <Grid container spacing={2}>
                        {(overview.builtInTools || []).map((tool) => (
                            <Grid item xs={12} md={6} xl={4} key={tool.toolCode}>
                                <BuiltInToolCard tool={tool} onSave={handleBuiltInSaved} />
                            </Grid>
                        ))}
                        {!overview?.builtInTools?.length ? <EmptyData /> : null}
                    </Grid>
                </AiConsolePanel>
                <Box sx={{ mt: 1.5 }}>
                    <AiConsolePanel
                        title="外部 MCP 服务"
                        subtitle="默认只需要录入服务名称、地址和用途。系统会优先自动识别协议,连接成功后再展示发现到的工具。"
                        action={(
                            <Button variant="contained" startIcon={<AddIcon />} onClick={() => { setEditingService(null); setServiceDialogOpen(true); }}>
                                新增服务
                            </Button>
                        )}
                        minHeight={280}
                    >
                        <Grid container spacing={2}>
                            {(overview.externalServices || []).map((service) => (
                                <Grid item xs={12} md={6} xl={4} key={service.id}>
                                    <ExternalServiceCard
                                        service={service}
                                        onEdit={(item) => { setEditingService(item); setServiceDialogOpen(true); }}
                                        onTest={handleTestService}
                                        onInspect={handleInspectService}
                                        onRemove={handleRemoveService}
                                    />
                                </Grid>
                            ))}
                            {!overview?.externalServices?.length ? <EmptyData /> : null}
                        </Grid>
                    </AiConsolePanel>
                </Box>
                <Box sx={{ mt: 1.5 }}>
                    <AiConsolePanel
                        title="工具使用策略"
                        subtitle="内置工具按用途预设自动参与聊天或诊断;外部服务在连接成功后可以查看其实际工具目录并做调用预览。"
                        minHeight={120}
                    >
                        <Stack direction="row" spacing={1} flexWrap="wrap">
                            <Chip icon={<BuildIcon />} label="内置工具默认自动参与诊断" color="primary" variant="outlined" />
                            <Chip icon={<HubIcon />} label="外部服务默认按用途预设参与运行时工具选择" color="primary" variant="outlined" />
                            <Chip label="高级规则已折叠到各卡片内部" variant="outlined" />
                        </Stack>
                    </AiConsolePanel>
                </Box>
                <Box sx={{ mt: 1.5 }}>
                    <ExternalToolsPanel
                        selectedService={selectedService}
                        tools={serviceTools}
                        preview={preview}
                        onPreview={handlePreviewTool}
                    />
                </Box>
            </AiConsoleLayout>
            <ServiceDialog
                open={serviceDialogOpen}
                record={editingService}
                onClose={() => {
                    setServiceDialogOpen(false);
                    setEditingService(null);
                }}
                onSaved={handleServiceSaved}
            />
        </Box>
    );
};
export default AiMcpMountList;
rsf-admin/src/page/system/aiMcpMount/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiMcpMountList from "./AiMcpMountList";
import AiMcpMountEdit from "./AiMcpMountEdit";
export default {
    list: AiMcpMountList,
    edit: AiMcpMountEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.name || record.mountCode || ''}`
};
rsf-admin/src/page/system/aiParam/AiParamList.jsx
@@ -1,42 +1,25 @@
import React, { useState } from "react";
import {
    List,
    DatagridConfigurable,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    BulkDeleteButton,
    WrapperField,
    TextField,
    NumberField,
    DateField,
    BooleanField,
    TextInput,
    DateInput,
    SelectInput,
    useListContext,
    Pagination,
    EditButton,
    DeleteButton,
} from 'react-admin';
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import { Box, Chip, Grid, Stack, Typography } from '@mui/material';
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import MyExportButton from '@/page/components/MyExportButton';
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import AiParamCreate from "./AiParamCreate";
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
        height: '.9em'
    },
    '& .RaDatagrid-row': {
        cursor: 'auto'
    },
    '& .opt': {
        width: 200
    },
}));
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
const filters = [
    <SearchInput source="condition" alwaysOn />,
@@ -63,14 +46,123 @@
            { id: '0', name: 'common.enums.statusFalse' },
        ]}
    />,
]
];
const providerLabel = (provider) => {
    if (provider === 'openai') {
        return 'OpenAI Compatible';
    }
    if (provider === 'mock') {
        return 'Mock';
    }
    return provider || '未配置';
};
const AiParamBoard = () => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const enabledCount = records.filter((item) => item.status === 1).length;
    const defaultCount = records.filter((item) => item.defaultFlag === 1).length;
    const openaiCount = records.filter((item) => item.provider === 'openai').length;
    const mockCount = records.filter((item) => item.provider === 'mock').length;
    if (!isLoading && !records.length) {
        return <EmptyData />;
    }
    return (
        <AiConsoleLayout
            title="AI参数"
            subtitle="保持当前系统后台的白底卡片和轻边框风格,用卡片方式展示模型配置概况,方便和其他 AI 页面统一查看。"
            stats={[
                { label: '参数总数', value: records.length },
                { label: '启用', value: enabledCount },
                { label: '默认模型', value: defaultCount },
                { label: 'OpenAI / Mock', value: `${openaiCount} / ${mockCount}` },
            ]}
        >
            <AiConsolePanel
                title="模型配置"
                subtitle="展示模型编码、供应商、上下文轮数和默认状态;创建、编辑、删除仍沿用原系统的弹窗与编辑页。"
                minHeight={460}
            >
                <Box
                    sx={{
                        display: 'grid',
                        gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
                        gap: 2,
                    }}
                >
                    {records.map((record) => (
                        <Box key={record.id}>
                            <Box sx={aiCardSx(record.defaultFlag === 1)}>
                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                    <Box>
                                        <Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
                                            {record.name || record.modelCode || record.uuid}
                                        </Typography>
                                        <Typography variant="caption" color="text.secondary">
                                            {record.modelCode || '未填写模型编码'}
                                        </Typography>
                                    </Box>
                                    <Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
                                        <Chip size="small" color={record.status === 1 ? 'success' : 'default'} label={record.status === 1 ? '启用' : '停用'} />
                                        {record.defaultFlag === 1 ? <Chip size="small" color="primary" label="默认" /> : null}
                                    </Stack>
                                </Stack>
                                <Grid container spacing={1.25} sx={{ mt: 1 }}>
                                    <Grid item xs={6}>
                                        <Typography variant="caption" color="text.secondary">供应商</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25 }}>{providerLabel(record.provider)}</Typography>
                                    </Grid>
                                    <Grid item xs={6}>
                                        <Typography variant="caption" color="text.secondary">模型名</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25 }}>{record.modelName || '-'}</Typography>
                                    </Grid>
                                    <Grid item xs={6}>
                                        <Typography variant="caption" color="text.secondary">上下文轮数</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25 }}>{record.maxContextMessages || 0}</Typography>
                                    </Grid>
                                    <Grid item xs={6}>
                                        <Typography variant="caption" color="text.secondary">排序</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25 }}>{record.sort || 0}</Typography>
                                    </Grid>
                                    <Grid item xs={12}>
                                        <Typography variant="caption" color="text.secondary">聊天地址</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25, wordBreak: 'break-all' }}>
                                            {record.chatUrl || '未配置'}
                                        </Typography>
                                    </Grid>
                                    <Grid item xs={12}>
                                        <Typography variant="caption" color="text.secondary">备注</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25, minHeight: 42 }}>
                                            {record.memo || '未填写备注'}
                                        </Typography>
                                    </Grid>
                                </Grid>
                                <Stack direction="row" spacing={1} sx={{ mt: 1.5 }}>
                                    <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
                                    <DeleteButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} mutationMode={OPERATE_MODE} />
                                </Stack>
                            </Box>
                        </Box>
                    ))}
                </Box>
                <Box sx={{ mt: 2 }}>
                    <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                </Box>
            </AiConsolePanel>
        </AiConsoleLayout>
    );
};
const AiParamList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <Box display="flex">
        <Box display="flex" sx={{ width: '100%' }}>
            <List
                sx={{ width: '100%', flexGrow: 1 }}
                title={"menu.aiParam"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
@@ -84,30 +176,9 @@
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
                pagination={false}
            >
                <StyledDatagrid
                    preferenceKey='aiParam'
                    bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />}
                    rowClick={false}
                    omit={['id', 'createTime', 'memo', 'statusBool', 'defaultFlagBool']}
                >
                    <NumberField source="id" />
                    <TextField source="uuid" label="table.field.aiParam.uuid" />
                    <TextField source="name" label="table.field.aiParam.name" />
                    <TextField source="modelCode" label="table.field.aiParam.modelCode" />
                    <TextField source="provider" label="table.field.aiParam.provider" />
                    <TextField source="modelName" label="table.field.aiParam.modelName" />
                    <NumberField source="maxContextMessages" label="table.field.aiParam.maxContextMessages" />
                    <NumberField source="sort" label="table.field.aiParam.sort" />
                    <BooleanField source="defaultFlagBool" label="table.field.aiParam.defaultFlag" sortable={false} />
                    <BooleanField source="statusBool" label="common.field.status" sortable={false} />
                    <DateField source="updateTime" label="common.field.updateTime" showTime />
                    <TextField source="memo" label="common.field.memo" sortable={false} />
                    <WrapperField cellClassName="opt" label="common.field.opt">
                        <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} />
                        <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} />
                    </WrapperField>
                </StyledDatagrid>
                <AiParamBoard />
            </List>
            <AiParamCreate
                open={createDialog}
rsf-admin/src/page/system/aiPrompt/AiPromptCreate.jsx
New file
@@ -0,0 +1,87 @@
import React from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    useNotify,
    Form,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Stack,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
import StatusSelectInput from "@/page/components/StatusSelectInput";
import MemoInput from "@/page/components/MemoInput";
const sceneChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const AiPromptCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async () => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <CreateBase
            record={{ sceneCode: 'system_diagnose', status: 1, publishedFlag: 0 }}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog open={open} onClose={handleClose} fullWidth disableRestoreFocus maxWidth="md">
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        {translate('create.title')}
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={6}><SelectInput source="sceneCode" label="场景" choices={sceneChoices} fullWidth /></Grid>
                            <Grid item xs={6}><TextInput source="templateName" label="模板名称" fullWidth /></Grid>
                            <Grid item xs={12}><TextInput source="basePrompt" label="基础提示词" fullWidth multiline minRows={4} /></Grid>
                            <Grid item xs={12}><TextInput source="toolPrompt" label="工具提示词" fullWidth multiline minRows={4} /></Grid>
                            <Grid item xs={12}><TextInput source="outputPrompt" label="输出提示词" fullWidth multiline minRows={4} /></Grid>
                            <Grid item xs={6}><NumberInput source="versionNo" label="版本号" fullWidth /></Grid>
                            <Grid item xs={6}><StatusSelectInput fullWidth /></Grid>
                            <Grid item xs={12}><Stack direction="column" spacing={1} width={'100%'}><MemoInput /></Stack></Grid>
                        </Grid>
                    </DialogContent>
                    <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}>
                            <SaveButton />
                        </Toolbar>
                    </DialogActions>
                </Form>
            </Dialog>
        </CreateBase>
    )
}
export default AiPromptCreate;
rsf-admin/src/page/system/aiPrompt/AiPromptEdit.jsx
New file
@@ -0,0 +1,64 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    DeleteButton,
} from 'react-admin';
import { Stack, Grid, Typography } from '@mui/material';
import { EDIT_MODE } from '@/config/setting';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
import MemoInput from "@/page/components/MemoInput";
import StatusSelectInput from "@/page/components/StatusSelectInput";
const sceneChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const FormToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton mutationMode="optimistic" />
    </Toolbar>
);
const AiPromptEdit = () => (
    <Edit redirect="list" mutationMode={EDIT_MODE} actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm shouldUnregister warnWhenUnsavedChanges toolbar={<FormToolbar />} mode="onTouched">
            <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12} md={8}>
                    <Typography variant="h6" gutterBottom>主要</Typography>
                    <Stack direction='row' gap={2}>
                        <SelectInput source="sceneCode" label="场景" choices={sceneChoices} />
                        <TextInput source="templateName" label="模板名称" />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <NumberInput source="versionNo" label="版本号" />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="basePrompt" label="基础提示词" fullWidth multiline minRows={4} />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="toolPrompt" label="工具提示词" fullWidth multiline minRows={4} />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="outputPrompt" label="输出提示词" fullWidth multiline minRows={4} />
                    </Stack>
                </Grid>
                <Grid item xs={12} md={4}>
                    <Typography variant="h6" gutterBottom>通用</Typography>
                    <StatusSelectInput />
                    <MemoInput />
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
)
export default AiPromptEdit;
rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx
New file
@@ -0,0 +1,383 @@
import React, { useEffect, useMemo, useState } from "react";
import {
    List,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    FilterButton,
    TextInput,
    DateInput,
    SelectInput,
    useNotify,
    useRefresh,
    useListContext,
    Pagination,
    EditButton,
} from 'react-admin';
import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, Grid, InputLabel, MenuItem, Select, Stack, TextField as MuiTextField, Typography } from '@mui/material';
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import MyExportButton from '@/page/components/MyExportButton';
import { DEFAULT_PAGE_SIZE } from '@/config/setting';
import AiPromptCreate from "./AiPromptCreate";
import request from "@/utils/request";
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
const sceneChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const sceneLabels = {
    general_chat: '通用对话',
    system_diagnose: '系统诊断',
};
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <SelectInput source="sceneCode" label="场景" choices={sceneChoices} />,
    <TextInput source="templateName" label="模板名称" />,
    <SelectInput
        source="publishedFlag"
        label="发布状态"
        choices={[
            { id: '1', name: '已发布' },
            { id: '0', name: '草稿' },
        ]}
    />,
    <SelectInput
        label="common.field.status"
        source="status"
        choices={[
            { id: '1', name: 'common.enums.statusTrue' },
            { id: '0', name: 'common.enums.statusFalse' },
        ]}
    />,
];
const PromptBoard = ({ onCreateDraft }) => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const refresh = useRefresh();
    const notify = useNotify();
    const [selectedScene, setSelectedScene] = useState('');
    const [versionDialog, setVersionDialog] = useState(false);
    const [activeRecord, setActiveRecord] = useState(null);
    const [logs, setLogs] = useState([]);
    const [compareId, setCompareId] = useState('');
    const [compareData, setCompareData] = useState(null);
    const groupedScenes = useMemo(() => {
        const map = {};
        records.forEach((item) => {
            const code = item.sceneCode || 'unknown';
            if (!map[code]) {
                map[code] = [];
            }
            map[code].push(item);
        });
        return map;
    }, [records]);
    const sceneCodes = Object.keys(groupedScenes).sort();
    const selectedRecords = groupedScenes[selectedScene] || [];
    const publishedCount = records.filter((item) => item.publishedFlag === 1).length;
    const draftCount = records.filter((item) => item.publishedFlag !== 1).length;
    useEffect(() => {
        if (!selectedScene && sceneCodes.length) {
            setSelectedScene(sceneCodes[0]);
        }
        if (selectedScene && !groupedScenes[selectedScene] && sceneCodes.length) {
            setSelectedScene(sceneCodes[0]);
        }
    }, [selectedScene, sceneCodes, groupedScenes]);
    const openVersionDialog = async (record) => {
        try {
            const logRes = await request.get(`/ai/prompt/publish-log/list?sceneCode=${record.sceneCode}`);
            setActiveRecord(record);
            setLogs(logRes.data?.data || []);
            setCompareId('');
            setCompareData(null);
            setVersionDialog(true);
        } catch (error) {
            notify(error.message || '加载版本日志失败', { type: 'error' });
        }
    };
    const handlePublish = async (record) => {
        try {
            await request.post('/ai/prompt/publish', { id: record.id });
            notify('common.response.success');
            refresh();
        } catch (error) {
            notify(error.message || '操作失败', { type: 'error' });
        }
    };
    const handleRollback = async (record) => {
        try {
            await request.post('/ai/prompt/rollback', { id: record.id });
            notify('common.response.success');
            refresh();
        } catch (error) {
            notify(error.message || '操作失败', { type: 'error' });
        }
    };
    const handleCopy = async (record) => {
        try {
            await request.post('/ai/prompt/copy', { id: record.id });
            notify('common.response.success');
            refresh();
        } catch (error) {
            notify(error.message || '操作失败', { type: 'error' });
        }
    };
    const handleCompare = async () => {
        if (!activeRecord || !compareId) {
            return;
        }
        try {
            const res = await request.get(`/ai/prompt/compare?leftId=${activeRecord.id}&rightId=${compareId}`);
            setCompareData(res.data?.data || null);
        } catch (error) {
            notify(error.message || '加载对比失败', { type: 'error' });
        }
    };
    if (!isLoading && !records.length) {
        return <EmptyData onClick={onCreateDraft} />;
    }
    return (
        <>
            <AiConsoleLayout
                title="Prompt 配置中心"
                subtitle="页面结构参考 zy-wcs-master 的 Prompt 中心,保留原系统的按钮、编辑页和权限模型,重点增强场景感知、版本阅读和运营动作展示。"
                actions={[
                    <Button key="draft" variant="contained" onClick={onCreateDraft}>新建草稿</Button>,
                ]}
                stats={[
                    { label: '场景数', value: sceneCodes.length },
                    { label: '版本总数', value: records.length },
                    { label: '已发布', value: publishedCount },
                    { label: '草稿', value: draftCount },
                ]}
            >
                <Grid container spacing={2}>
                    <Grid item xs={12} md={3}>
                        <AiConsolePanel
                            title="场景"
                            subtitle="每个场景只会有一个已发布版本,诊断和聊天运行时会直接读取它。"
                            minHeight={520}
                        >
                            <Stack spacing={1.5}>
                                {sceneCodes.map((code) => {
                                    const list = groupedScenes[code] || [];
                                    const published = list.find((item) => item.publishedFlag === 1);
                                    const drafts = list.filter((item) => item.publishedFlag !== 1).length;
                                    return (
                                        <Box key={code} sx={{ ...aiCardSx(selectedScene === code), cursor: 'pointer' }} onClick={() => setSelectedScene(code)}>
                                            <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                                <Box>
                                                    <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                                        {sceneLabels[code] || code}
                                                    </Typography>
                                                    <Typography variant="caption" sx={{ color: '#8aa0b7' }}>{code}</Typography>
                                                </Box>
                                                <Chip size="small" color={published ? 'success' : 'default'} label={published ? `v${published.versionNo}` : '未发布'} />
                                            </Stack>
                                            <Grid container spacing={1} sx={{ mt: 1 }}>
                                                <Grid item xs={6}>
                                                    <Box sx={{ p: 1, borderRadius: 2, border: '1px solid #e7eef7', backgroundColor: '#fff' }}>
                                                        <Typography variant="caption" sx={{ color: '#7f92a8' }}>版本数</Typography>
                                                        <Typography variant="h6" sx={{ mt: 0.25, color: '#2a3e55' }}>{list.length}</Typography>
                                                    </Box>
                                                </Grid>
                                                <Grid item xs={6}>
                                                    <Box sx={{ p: 1, borderRadius: 2, border: '1px solid #e7eef7', backgroundColor: '#fff' }}>
                                                        <Typography variant="caption" sx={{ color: '#7f92a8' }}>草稿数</Typography>
                                                        <Typography variant="h6" sx={{ mt: 0.25, color: '#2a3e55' }}>{drafts}</Typography>
                                                    </Box>
                                                </Grid>
                                            </Grid>
                                        </Box>
                                    );
                                })}
                            </Stack>
                        </AiConsolePanel>
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <AiConsolePanel
                            title="版本列表"
                            subtitle={`当前场景:${sceneLabels[selectedScene] || selectedScene || '未选择场景'}`}
                            action={<Button size="small" variant="outlined" onClick={onCreateDraft}>新建草稿</Button>}
                            minHeight={520}
                        >
                            <Stack spacing={1.5}>
                                {selectedRecords.map((record) => (
                                    <Box key={record.id} sx={aiCardSx(record.publishedFlag === 1)}>
                                        <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                            <Box>
                                                <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                                    {record.templateName || `${sceneLabels[record.sceneCode] || record.sceneCode} v${record.versionNo}`}
                                                </Typography>
                                                <Typography variant="caption" sx={{ color: '#8093a8' }}>
                                                    版本 v{record.versionNo} · 更新时间 {record.updateTime || '-'}
                                                </Typography>
                                            </Box>
                                            <Stack direction="row" spacing={0.75}>
                                                <Chip size="small" color={record.publishedFlag === 1 ? 'warning' : 'default'} label={record.publishedFlag === 1 ? '已发布' : '草稿'} />
                                                <Chip size="small" color={record.status === 1 ? 'success' : 'default'} label={record.status === 1 ? '启用' : '停用'} />
                                            </Stack>
                                        </Stack>
                                        <Typography variant="body2" sx={{ mt: 1.25, minHeight: 48, color: '#31465d' }}>
                                            {record.memo || '未填写版本备注'}
                                        </Typography>
                                        <Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1.5 }}>
                                            <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
                                            <Button size="small" variant="outlined" onClick={() => openVersionDialog(record)}>版本</Button>
                                            <Button size="small" variant="outlined" onClick={() => handleCopy(record)}>复制</Button>
                                            <Button size="small" variant="outlined" onClick={() => handlePublish(record)}>发布</Button>
                                            <Button size="small" variant="outlined" onClick={() => handleRollback(record)}>回滚</Button>
                                        </Stack>
                                    </Box>
                                ))}
                            </Stack>
                            <Box sx={{ mt: 2 }}>
                                <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                            </Box>
                        </AiConsolePanel>
                    </Grid>
                    <Grid item xs={12} md={5}>
                        <AiConsolePanel
                            title="运营提示"
                            subtitle="当前版本编辑仍进入原有编辑页,这里主要承接版本阅读、对比和发布日志预览。"
                            minHeight={520}
                        >
                            <Box sx={{ ...aiCardSx(false), minHeight: 140 }}>
                                <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>使用建议</Typography>
                                <Typography variant="body2" sx={{ mt: 1, color: '#31465d', lineHeight: 1.8 }}>
                                    先在左侧切换场景,再在中间挑选草稿或线上版本。需要深度查看发布轨迹、做两版 Prompt 文本对比时,点击“版本”进入运营弹窗。
                                </Typography>
                            </Box>
                            <Box sx={{ ...aiCardSx(true), mt: 1.5, minHeight: 230 }}>
                                <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>当前场景摘要</Typography>
                                <Stack spacing={1} sx={{ mt: 1.25 }}>
                                    <Typography variant="body2" sx={{ color: '#31465d' }}>
                                        场景编码:{selectedScene || '-'}
                                    </Typography>
                                    <Typography variant="body2" sx={{ color: '#31465d' }}>
                                        发布版本:{(selectedRecords.find((item) => item.publishedFlag === 1)?.versionNo) ? `v${selectedRecords.find((item) => item.publishedFlag === 1)?.versionNo}` : '暂无'}
                                    </Typography>
                                    <Typography variant="body2" sx={{ color: '#31465d' }}>
                                        草稿数量:{selectedRecords.filter((item) => item.publishedFlag !== 1).length}
                                    </Typography>
                                    <Typography variant="body2" sx={{ color: '#31465d' }}>
                                        启用版本:{selectedRecords.filter((item) => item.status === 1).length}
                                    </Typography>
                                </Stack>
                            </Box>
                        </AiConsolePanel>
                    </Grid>
                </Grid>
            </AiConsoleLayout>
            <Dialog open={versionDialog} onClose={() => setVersionDialog(false)} fullWidth maxWidth="lg">
                <DialogTitle>Prompt 版本运营</DialogTitle>
                <DialogContent>
                    <Stack spacing={3}>
                        <Typography variant="body2">
                            当前模板:{activeRecord?.templateName || '-'} / 版本 {activeRecord?.versionNo || '-'}
                        </Typography>
                        <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
                            <Box flex={1}>
                                <Typography variant="subtitle1" gutterBottom>发布日志</Typography>
                                {logs.map((item) => (
                                    <Box key={item.id} sx={{ borderBottom: '1px solid #eee', pb: 1, mb: 1 }}>
                                        <Typography variant="body2">{item.actionType} / v{item.versionNo} / {item.templateName}</Typography>
                                        <Typography variant="caption" color="text.secondary">{item.actionDesc} / {item.createTime}</Typography>
                                    </Box>
                                ))}
                            </Box>
                            <Box flex={1}>
                                <Typography variant="subtitle1" gutterBottom>文本对比</Typography>
                                <Stack spacing={1.5}>
                                    <FormControl sx={{ minWidth: 220 }}>
                                        <InputLabel id="prompt-compare-label">对比版本</InputLabel>
                                        <Select
                                            labelId="prompt-compare-label"
                                            label="对比版本"
                                            value={compareId}
                                            onChange={(event) => setCompareId(event.target.value)}
                                        >
                                            {selectedRecords.filter((item) => item.id !== activeRecord?.id).map((item) => (
                                                <MenuItem key={item.id} value={item.id}>v{item.versionNo} / {item.templateName}</MenuItem>
                                            ))}
                                        </Select>
                                    </FormControl>
                                    <Button variant="outlined" onClick={handleCompare}>加载对比</Button>
                                </Stack>
                            </Box>
                        </Stack>
                        {compareData ? (
                            <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
                                <Box flex={1}>
                                    <Typography variant="subtitle1" gutterBottom>当前版本</Typography>
                                    <MuiTextField label="基础提示词" fullWidth multiline minRows={5} value={compareData.left?.basePrompt || ''} disabled margin="dense" />
                                    <MuiTextField label="工具提示词" fullWidth multiline minRows={5} value={compareData.left?.toolPrompt || ''} disabled margin="dense" />
                                    <MuiTextField label="输出提示词" fullWidth multiline minRows={5} value={compareData.left?.outputPrompt || ''} disabled margin="dense" />
                                </Box>
                                <Box flex={1}>
                                    <Typography variant="subtitle1" gutterBottom>对比版本</Typography>
                                    <MuiTextField label="基础提示词" fullWidth multiline minRows={5} value={compareData.right?.basePrompt || ''} disabled margin="dense" />
                                    <MuiTextField label="工具提示词" fullWidth multiline minRows={5} value={compareData.right?.toolPrompt || ''} disabled margin="dense" />
                                    <MuiTextField label="输出提示词" fullWidth multiline minRows={5} value={compareData.right?.outputPrompt || ''} disabled margin="dense" />
                                </Box>
                            </Stack>
                        ) : null}
                    </Stack>
                </DialogContent>
                <DialogActions>
                    <Button onClick={() => setVersionDialog(false)}>关闭</Button>
                </DialogActions>
            </Dialog>
        </>
    );
};
const AiPromptList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <Box display="flex" sx={{ width: '100%' }}>
            <List
                sx={{ width: '100%', flexGrow: 1 }}
                title={"menu.aiPrompt"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
                sort={{ field: "updateTime", order: "desc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='aiPrompt' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
                pagination={false}
            >
                <PromptBoard onCreateDraft={() => setCreateDialog(true)} />
            </List>
            <AiPromptCreate open={createDialog} setOpen={setCreateDialog} />
        </Box>
    )
}
export default AiPromptList;
rsf-admin/src/page/system/aiPrompt/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiPromptList from "./AiPromptList";
import AiPromptEdit from "./AiPromptEdit";
export default {
    list: AiPromptList,
    edit: AiPromptEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.templateName || record.sceneCode || ''}`
};
rsf-admin/src/page/system/aiRoute/AiRouteCreate.jsx
New file
@@ -0,0 +1,84 @@
import React from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    useNotify,
    Form,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Stack,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
import StatusSelectInput from "@/page/components/StatusSelectInput";
import MemoInput from "@/page/components/MemoInput";
const routeChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const AiRouteCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async () => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <CreateBase
            record={{ routeCode: 'general_chat', priority: 1, failCount: 0, successCount: 0, status: 1 }}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog open={open} onClose={handleClose} fullWidth disableRestoreFocus maxWidth="md">
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        {translate('create.title')}
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={6}><SelectInput source="routeCode" label="路由编码" choices={routeChoices} fullWidth /></Grid>
                            <Grid item xs={6}><TextInput source="modelCode" label="模型编码" fullWidth /></Grid>
                            <Grid item xs={6}><NumberInput source="priority" label="优先级" fullWidth /></Grid>
                            <Grid item xs={6}><StatusSelectInput fullWidth /></Grid>
                            <Grid item xs={12}><Stack direction="column" spacing={1} width={'100%'}><MemoInput /></Stack></Grid>
                        </Grid>
                    </DialogContent>
                    <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}>
                            <SaveButton />
                        </Toolbar>
                    </DialogActions>
                </Form>
            </Dialog>
        </CreateBase>
    )
}
export default AiRouteCreate;
rsf-admin/src/page/system/aiRoute/AiRouteEdit.jsx
New file
@@ -0,0 +1,60 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    DeleteButton,
} from 'react-admin';
import { Stack, Grid, Typography } from '@mui/material';
import { EDIT_MODE } from '@/config/setting';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
import MemoInput from "@/page/components/MemoInput";
import StatusSelectInput from "@/page/components/StatusSelectInput";
const routeChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const FormToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton mutationMode="optimistic" />
    </Toolbar>
);
const AiRouteEdit = () => (
    <Edit redirect="list" mutationMode={EDIT_MODE} actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm shouldUnregister warnWhenUnsavedChanges toolbar={<FormToolbar />} mode="onTouched">
            <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12} md={8}>
                    <Typography variant="h6" gutterBottom>主要</Typography>
                    <Stack direction='row' gap={2}>
                        <SelectInput source="routeCode" label="路由编码" choices={routeChoices} />
                        <TextInput source="modelCode" label="模型编码" />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <NumberInput source="priority" label="优先级" />
                        <NumberInput source="failCount" label="失败次数" />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <NumberInput source="successCount" label="成功次数" />
                        <TextInput source="cooldownUntil$" label="冷却截止" disabled />
                    </Stack>
                </Grid>
                <Grid item xs={12} md={4}>
                    <Typography variant="h6" gutterBottom>通用</Typography>
                    <StatusSelectInput />
                    <MemoInput />
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
)
export default AiRouteEdit;
rsf-admin/src/page/system/aiRoute/AiRouteList.jsx
New file
@@ -0,0 +1,190 @@
import React, { useState } from "react";
import {
    List,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextInput,
    DateInput,
    SelectInput,
    useNotify,
    useRefresh,
    useListContext,
    Pagination,
    DeleteButton,
} from 'react-admin';
import { Box, Button, Chip, Grid, Stack, Typography } from '@mui/material';
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import MyExportButton from '@/page/components/MyExportButton';
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import AiRouteCreate from "./AiRouteCreate";
import request from "@/utils/request";
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
const routeChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <SelectInput source="routeCode" label="路由编码" choices={routeChoices} />,
    <TextInput source="modelCode" label="模型编码" />,
    <SelectInput label="common.field.status" source="status" choices={[
        { id: '1', name: 'common.enums.statusTrue' },
        { id: '0', name: 'common.enums.statusFalse' },
    ]} />,
];
const RouteCardActions = ({ record }) => {
    const refresh = useRefresh();
    const notify = useNotify();
    const handleToggle = async () => {
        try {
            await request.post('/ai/route/toggle', { id: record.id, status: record.status === 1 ? 0 : 1 });
            notify('common.response.success');
            refresh();
        } catch (error) {
            notify(error.message || '操作失败', { type: 'error' });
        }
    };
    const handleReset = async () => {
        try {
            await request.post('/ai/route/reset', { id: record.id });
            notify('common.response.success');
            refresh();
        } catch (error) {
            notify(error.message || '操作失败', { type: 'error' });
        }
    };
    return (
        <Stack direction="row" spacing={1} flexWrap="wrap">
            <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
            <DeleteButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} mutationMode={OPERATE_MODE} />
            <Button size="small" variant="outlined" onClick={handleToggle}>{record.status === 1 ? '停用' : '启用'}</Button>
            <Button size="small" variant="outlined" onClick={handleReset}>重置</Button>
        </Stack>
    );
};
const RouteBoard = () => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const enabledCount = records.filter((item) => item.status === 1).length;
    const coolingCount = records.filter((item) => item.cooldownUntil).length;
    const failSwitchCount = records.filter((item) => (item.failCount || 0) > 0).length;
    const successCount = records.reduce((sum, item) => sum + (item.successCount || 0), 0);
    if (!isLoading && !records.length) {
        return <EmptyData />;
    }
    return (
        <AiConsoleLayout
            title="AI模型路由"
            subtitle="参考 zy-wcs-master 的 LLM 控制台信息层次,突出路由状态、冷却与成功失败计数,但保留现有系统的按钮、筛选和表单风格。"
            stats={[
                { label: '总路由', value: records.length },
                { label: '启用', value: enabledCount },
                { label: '冷却中', value: coolingCount },
                { label: '累计成功', value: successCount, helper: `已有失败记录 ${failSwitchCount} 条` },
            ]}
        >
            <AiConsolePanel
                title="路由卡片"
                subtitle="每张卡片代表一条模型候选,优先级越小越先命中;启停、重置会立即作用于后续请求。"
                minHeight={460}
            >
                <Grid container spacing={2}>
                    {records.map((record) => (
                        <Grid item xs={12} md={6} xl={4} key={record.id}>
                            <Box sx={aiCardSx(record.status === 1)}>
                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                    <Box>
                                        <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                            {record.modelCode}
                                        </Typography>
                                        <Typography variant="caption" sx={{ color: '#8093a8' }}>
                                            {record.routeCode} · 优先级 {record.priority}
                                        </Typography>
                                    </Box>
                                    <Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
                                        <Chip size="small" color={record.status === 1 ? 'success' : 'default'} label={record.status === 1 ? '启用' : '停用'} />
                                        {record.cooldownUntil ? <Chip size="small" color="warning" label="冷却中" /> : null}
                                    </Stack>
                                </Stack>
                                <Grid container spacing={1.25} sx={{ mt: 1 }}>
                                    <Grid item xs={6}>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>失败次数</Typography>
                                        <Typography variant="h6" sx={{ color: '#2f455c', mt: 0.25 }}>{record.failCount || 0}</Typography>
                                    </Grid>
                                    <Grid item xs={6}>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>成功次数</Typography>
                                        <Typography variant="h6" sx={{ color: '#2f455c', mt: 0.25 }}>{record.successCount || 0}</Typography>
                                    </Grid>
                                    <Grid item xs={12}>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>冷却截止</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25, color: '#31465d' }}>
                                            {record.cooldownUntil$ || '未进入冷却'}
                                        </Typography>
                                    </Grid>
                                    <Grid item xs={12}>
                                        <Typography variant="caption" sx={{ color: '#70839a' }}>备注</Typography>
                                        <Typography variant="body2" sx={{ mt: 0.25, color: '#31465d', minHeight: 42 }}>
                                            {record.memo || '未填写备注'}
                                        </Typography>
                                    </Grid>
                                </Grid>
                                <Box sx={{ mt: 1.5 }}>
                                    <RouteCardActions record={record} />
                                </Box>
                            </Box>
                        </Grid>
                    ))}
                </Grid>
                <Box sx={{ mt: 2 }}>
                    <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                </Box>
            </AiConsolePanel>
        </AiConsoleLayout>
    );
};
const AiRouteList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <Box display="flex" sx={{ width: '100%' }}>
            <List
                sx={{ width: '100%', flexGrow: 1 }}
                title={"menu.aiRoute"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
                sort={{ field: "priority", order: "asc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='aiRoute' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
                pagination={false}
            >
                <RouteBoard />
            </List>
            <AiRouteCreate open={createDialog} setOpen={setCreateDialog} />
        </Box>
    );
}
export default AiRouteList;
rsf-admin/src/page/system/aiRoute/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiRouteList from "./AiRouteList";
import AiRouteEdit from "./AiRouteEdit";
export default {
    list: AiRouteList,
    edit: AiRouteEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.routeCode || record.modelCode || ''}`
};
rsf-admin/src/page/system/aiToolConfig/AiToolConfigCreate.jsx
New file
@@ -0,0 +1,92 @@
import React from "react";
import {
    CreateBase,
    useTranslate,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    useNotify,
    Form,
} from 'react-admin';
import {
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Stack,
    Grid,
    Box,
} from '@mui/material';
import DialogCloseButton from "@/page/components/DialogCloseButton";
import StatusSelectInput from "@/page/components/StatusSelectInput";
import MemoInput from "@/page/components/MemoInput";
const sceneChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const enabledChoices = [
    { id: 1, name: '启用' },
    { id: 0, name: '停用' },
];
const AiToolConfigCreate = (props) => {
    const { open, setOpen } = props;
    const translate = useTranslate();
    const notify = useNotify();
    const handleClose = (event, reason) => {
        if (reason !== "backdropClick") {
            setOpen(false);
        }
    };
    const handleSuccess = async () => {
        setOpen(false);
        notify('common.response.success');
    };
    const handleError = async (error) => {
        notify(error.message || 'common.response.fail', { type: 'error', messageArgs: { _: error.message } });
    };
    return (
        <CreateBase
            record={{ sceneCode: 'system_diagnose', priority: 10, enabledFlag: 1, status: 1 }}
            mutationOptions={{ onSuccess: handleSuccess, onError: handleError }}
        >
            <Dialog open={open} onClose={handleClose} fullWidth disableRestoreFocus maxWidth="md">
                <Form>
                    <DialogTitle sx={{ position: 'sticky', top: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        {translate('create.title')}
                        <Box sx={{ position: 'absolute', top: 8, right: 8, zIndex: 1001 }}>
                            <DialogCloseButton onClose={handleClose} />
                        </Box>
                    </DialogTitle>
                    <DialogContent sx={{ mt: 2 }}>
                        <Grid container rowSpacing={2} columnSpacing={2}>
                            <Grid item xs={6}><SelectInput source="sceneCode" label="场景" choices={sceneChoices} fullWidth /></Grid>
                            <Grid item xs={6}><TextInput source="toolCode" label="工具编码" fullWidth /></Grid>
                            <Grid item xs={6}><TextInput source="toolName" label="工具名称" fullWidth /></Grid>
                            <Grid item xs={6}><NumberInput source="priority" label="优先级" fullWidth /></Grid>
                            <Grid item xs={6}><SelectInput source="enabledFlag" label="启用" choices={enabledChoices} fullWidth /></Grid>
                            <Grid item xs={6}><StatusSelectInput fullWidth /></Grid>
                            <Grid item xs={12}><TextInput source="toolPrompt" label="工具提示词" fullWidth multiline minRows={4} /></Grid>
                            <Grid item xs={12}><Stack direction="column" spacing={1} width={'100%'}><MemoInput /></Stack></Grid>
                        </Grid>
                    </DialogContent>
                    <DialogActions sx={{ position: 'sticky', bottom: 0, backgroundColor: 'background.paper', zIndex: 1000 }}>
                        <Toolbar sx={{ width: '100%', justifyContent: 'space-between' }}>
                            <SaveButton />
                        </Toolbar>
                    </DialogActions>
                </Form>
            </Dialog>
        </CreateBase>
    )
}
export default AiToolConfigCreate;
rsf-admin/src/page/system/aiToolConfig/AiToolConfigEdit.jsx
New file
@@ -0,0 +1,67 @@
import React from "react";
import {
    Edit,
    SimpleForm,
    TextInput,
    NumberInput,
    SaveButton,
    SelectInput,
    Toolbar,
    DeleteButton,
} from 'react-admin';
import { Stack, Grid, Typography } from '@mui/material';
import { EDIT_MODE } from '@/config/setting';
import EditBaseAside from "@/page/components/EditBaseAside";
import CustomerTopToolBar from "@/page/components/EditTopToolBar";
import MemoInput from "@/page/components/MemoInput";
import StatusSelectInput from "@/page/components/StatusSelectInput";
const sceneChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const enabledChoices = [
    { id: 1, name: '启用' },
    { id: 0, name: '停用' },
];
const FormToolbar = () => (
    <Toolbar sx={{ justifyContent: 'space-between' }}>
        <SaveButton />
        <DeleteButton mutationMode="optimistic" />
    </Toolbar>
);
const AiToolConfigEdit = () => (
    <Edit redirect="list" mutationMode={EDIT_MODE} actions={<CustomerTopToolBar />} aside={<EditBaseAside />}>
        <SimpleForm shouldUnregister warnWhenUnsavedChanges toolbar={<FormToolbar />} mode="onTouched">
            <Grid container width={{ xs: '100%', xl: '80%' }} rowSpacing={3} columnSpacing={3}>
                <Grid item xs={12} md={8}>
                    <Typography variant="h6" gutterBottom>主要</Typography>
                    <Stack direction='row' gap={2}>
                        <SelectInput source="sceneCode" label="场景" choices={sceneChoices} />
                        <TextInput source="toolCode" label="工具编码" />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="toolName" label="工具名称" />
                        <NumberInput source="priority" label="优先级" />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <SelectInput source="enabledFlag" label="启用" choices={enabledChoices} />
                    </Stack>
                    <Stack direction='row' gap={2}>
                        <TextInput source="toolPrompt" label="工具提示词" fullWidth multiline minRows={4} />
                    </Stack>
                </Grid>
                <Grid item xs={12} md={4}>
                    <Typography variant="h6" gutterBottom>通用</Typography>
                    <StatusSelectInput />
                    <MemoInput />
                </Grid>
            </Grid>
        </SimpleForm>
    </Edit>
)
export default AiToolConfigEdit;
rsf-admin/src/page/system/aiToolConfig/AiToolConfigList.jsx
New file
@@ -0,0 +1,134 @@
import React, { useState } from "react";
import {
    List,
    SearchInput,
    TopToolbar,
    SelectColumnsButton,
    EditButton,
    FilterButton,
    TextInput,
    DateInput,
    SelectInput,
    useListContext,
    Pagination,
    DeleteButton,
} from 'react-admin';
import { Box, Button, Chip, Grid, Stack, Typography } from '@mui/material';
import EmptyData from "@/page/components/EmptyData";
import MyCreateButton from "@/page/components/MyCreateButton";
import MyExportButton from '@/page/components/MyExportButton';
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
import AiToolConfigCreate from "./AiToolConfigCreate";
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
const sceneChoices = [
    { id: 'general_chat', name: '通用对话' },
    { id: 'system_diagnose', name: '系统诊断' },
];
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <DateInput label='common.time.after' source="timeStart" alwaysOn />,
    <DateInput label='common.time.before' source="timeEnd" alwaysOn />,
    <SelectInput source="sceneCode" label="场景" choices={sceneChoices} />,
    <TextInput source="toolCode" label="工具编码" />,
    <TextInput source="toolName" label="工具名称" />,
];
const ToolConfigBoard = () => {
    const { data, isLoading } = useListContext();
    const records = data || [];
    const enabledCount = records.filter((item) => item.enabledFlag === 1).length;
    const diagnoseCount = records.filter((item) => item.sceneCode === 'system_diagnose').length;
    const chatCount = records.filter((item) => item.sceneCode === 'general_chat').length;
    if (!isLoading && !records.length) {
        return <EmptyData />;
    }
    return (
        <AiConsoleLayout
            title="AI诊断工具中心"
            subtitle="参考 zy-wcs-master 的工作区布局,提供场景化工具编排、启停和提示词增强,但控件与交互仍保持当前系统的 react-admin 风格。"
            stats={[
                { label: '工具总数', value: records.length },
                { label: '已启用', value: enabledCount },
                { label: '诊断场景', value: diagnoseCount },
                { label: '通用对话', value: chatCount },
            ]}
        >
            <AiConsolePanel
                title="工具编排"
                subtitle="同一场景会按优先级执行,启用状态和工具提示词会直接进入诊断运行时。"
                minHeight={420}
            >
                <Grid container spacing={2}>
                    {records.map((record) => (
                        <Grid item xs={12} md={6} xl={4} key={record.id}>
                            <Box sx={aiCardSx(record.enabledFlag === 1)}>
                                <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                    <Box>
                                        <Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
                                            {record.toolName || record.toolCode}
                                        </Typography>
                                        <Typography variant="caption" sx={{ color: '#8093a8' }}>
                                            {record.sceneCode} · {record.toolCode}
                                        </Typography>
                                    </Box>
                                    <Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
                                        <Chip size="small" color={record.enabledFlag === 1 ? 'success' : 'default'} label={record.enabledFlag === 1 ? '启用' : '停用'} />
                                        <Chip size="small" color={record.status === 1 ? 'primary' : 'default'} label={`P${record.priority || 0}`} />
                                    </Stack>
                                </Stack>
                                <Box sx={{ mt: 1.5 }}>
                                    <Typography variant="caption" sx={{ color: '#70839a' }}>工具提示词</Typography>
                                    <Typography variant="body2" sx={{ mt: 0.5, color: '#31465d', minHeight: 72 }}>
                                        {record.toolPrompt || '未配置附加提示词,将只使用默认工具摘要。'}
                                    </Typography>
                                </Box>
                                <Stack direction="row" spacing={1} sx={{ mt: 1.5 }}>
                                    <EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
                                    <DeleteButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} mutationMode={OPERATE_MODE} />
                                </Stack>
                            </Box>
                        </Grid>
                    ))}
                </Grid>
                <Box sx={{ mt: 2 }}>
                    <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
                </Box>
            </AiConsolePanel>
        </AiConsoleLayout>
    );
};
const AiToolConfigList = () => {
    const [createDialog, setCreateDialog] = useState(false);
    return (
        <Box display="flex" sx={{ width: '100%' }}>
            <List
                sx={{ width: '100%', flexGrow: 1 }}
                title={"menu.aiToolConfig"}
                empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
                filters={filters}
                sort={{ field: "priority", order: "asc" }}
                actions={(
                    <TopToolbar>
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='aiToolConfig' />
                        <MyExportButton />
                    </TopToolbar>
                )}
                perPage={DEFAULT_PAGE_SIZE}
                pagination={false}
            >
                <ToolConfigBoard />
            </List>
            <AiToolConfigCreate open={createDialog} setOpen={setCreateDialog} />
        </Box>
    )
}
export default AiToolConfigList;
rsf-admin/src/page/system/aiToolConfig/index.jsx
New file
@@ -0,0 +1,13 @@
import {
    ShowGuesser,
} from "react-admin";
import AiToolConfigList from "./AiToolConfigList";
import AiToolConfigEdit from "./AiToolConfigEdit";
export default {
    list: AiToolConfigList,
    edit: AiToolConfigEdit,
    show: ShowGuesser,
    recordRepresentation: (record) => `${record.toolName || record.toolCode || ''}`
};
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/AiGatewayProperties.java
@@ -12,7 +12,7 @@
@ConfigurationProperties(prefix = "gateway.ai")
public class AiGatewayProperties {
    private String defaultModelCode = "mock-general";
    private String defaultModelCode = "deepseek-ai/DeepSeek-V3.2";
    private Integer connectTimeoutMillis = 10000;
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/config/WebAsyncConfig.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.ai.gateway.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebAsyncConfig implements WebMvcConfigurer {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // StreamingResponseBody is used for long-lived model streams; disable the MVC async timeout.
        configurer.setDefaultTimeout(0L);
    }
}
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/controller/AiGatewayController.java
@@ -4,6 +4,8 @@
import com.vincent.rsf.ai.gateway.service.AiGatewayService;
import com.vincent.rsf.ai.gateway.service.GatewayStreamEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -13,11 +15,15 @@
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
@RestController
@RequestMapping("/internal/chat")
public class AiGatewayController {
    private static final Logger logger = LoggerFactory.getLogger(AiGatewayController.class);
    @Resource
    private AiGatewayService aiGatewayService;
@@ -27,16 +33,106 @@
    @PostMapping(value = "/stream", produces = "application/x-ndjson")
    public StreamingResponseBody stream(@RequestBody GatewayChatRequest request) {
        return outputStream -> {
            logger.info("AI gateway controller stream opened: sessionId={}, routeCode={}, attemptNo={}, modelCode={}",
                    request.getSessionId(),
                    request.getRouteCode(),
                    request.getAttemptNo(),
                    request.getModelCode());
            AtomicBoolean streaming = new AtomicBoolean(true);
            Object writeLock = new Object();
            Thread heartbeatThread = new Thread(() -> {
                while (streaming.get()) {
                    try {
                        Thread.sleep(10000L);
                        if (!streaming.get()) {
                            break;
                        }
                        String json = objectMapper.writeValueAsString(new GatewayStreamEvent()
                                .setType("ping")
                                .setModelCode(request.getModelCode())
                                .setResponseTime(System.currentTimeMillis())) + "\n";
                        synchronized (writeLock) {
                            outputStream.write(json.getBytes(StandardCharsets.UTF_8));
                            outputStream.flush();
                        }
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        logger.info("AI gateway heartbeat interrupted: sessionId={}, routeCode={}, attemptNo={}, modelCode={}",
                                request.getSessionId(),
                                request.getRouteCode(),
                                request.getAttemptNo(),
                                request.getModelCode());
                        break;
                    } catch (Exception e) {
                        logger.warn("AI gateway heartbeat write failed: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, message={}",
                                request.getSessionId(),
                                request.getRouteCode(),
                                request.getAttemptNo(),
                                request.getModelCode(),
                                e.getMessage());
                        break;
                    }
                }
            }, "ai-gateway-heartbeat-" + (request.getSessionId() == null ? "unknown" : request.getSessionId()));
            heartbeatThread.setDaemon(true);
            heartbeatThread.start();
            try {
                aiGatewayService.stream(request, event -> {
                    String json = objectMapper.writeValueAsString(event) + "\n";
                    outputStream.write(json.getBytes(StandardCharsets.UTF_8));
                    outputStream.flush();
                    synchronized (writeLock) {
                        outputStream.write(json.getBytes(StandardCharsets.UTF_8));
                        outputStream.flush();
                    }
                });
            } catch (Exception e) {
                if (isInterruptedError(e)) {
                    logger.warn("AI gateway controller stream interrupted: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, message={}",
                            request.getSessionId(),
                            request.getRouteCode(),
                            request.getAttemptNo(),
                            request.getModelCode(),
                            e.getMessage());
                    return;
                }
                logger.error("AI gateway controller stream failed: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, message={}",
                        request.getSessionId(),
                        request.getRouteCode(),
                        request.getAttemptNo(),
                        request.getModelCode(),
                        e.getMessage(),
                        e);
                throw new IOException(e);
            } finally {
                streaming.set(false);
                heartbeatThread.interrupt();
                logger.info("AI gateway controller stream closed: sessionId={}, routeCode={}, attemptNo={}, modelCode={}",
                        request.getSessionId(),
                        request.getRouteCode(),
                        request.getAttemptNo(),
                        request.getModelCode());
            }
        };
    }
    private boolean isInterruptedError(Throwable throwable) {
        Throwable current = throwable;
        while (current != null) {
            if (current instanceof InterruptedException || current instanceof InterruptedIOException) {
                return true;
            }
            String message = current.getMessage();
            if (message != null) {
                String normalized = message.toLowerCase();
                if (normalized.contains("interrupted")
                        || normalized.contains("broken pipe")
                        || normalized.contains("connection reset")
                        || normalized.contains("forcibly closed")) {
                    return true;
                }
            }
            current = current.getCause();
        }
        return false;
    }
}
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/dto/GatewayChatRequest.java
@@ -13,6 +13,10 @@
    private String modelCode;
    private String routeCode;
    private Integer attemptNo;
    private String systemPrompt;
    private String chatUrl;
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/AiGatewayService.java
@@ -5,12 +5,15 @@
import com.vincent.rsf.ai.gateway.dto.GatewayChatRequest;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
@@ -23,6 +26,8 @@
@Service
public class AiGatewayService {
    private static final Logger logger = LoggerFactory.getLogger(AiGatewayService.class);
    @Resource
    private AiGatewayProperties aiGatewayProperties;
    @Resource
@@ -34,7 +39,20 @@
    public void stream(GatewayChatRequest request, EventConsumer consumer) throws Exception {
        AiGatewayProperties.ModelConfig modelConfig = resolveModel(request);
        logger.info("AI gateway stream start: sessionId={}, routeCode={}, attemptNo={}, requestModelCode={}, resolvedModelCode={}, provider={}",
                request.getSessionId(),
                request.getRouteCode(),
                request.getAttemptNo(),
                request.getModelCode(),
                modelConfig == null ? null : modelConfig.getCode(),
                modelConfig == null ? null : modelConfig.getProvider());
        if (modelConfig == null || modelConfig.getChatUrl() == null || modelConfig.getChatUrl().trim().isEmpty()) {
            logger.info("AI gateway use mock stream: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, provider={}",
                    request.getSessionId(),
                    request.getRouteCode(),
                    request.getAttemptNo(),
                    modelConfig == null ? request.getModelCode() : modelConfig.getCode(),
                    modelConfig == null ? "mock" : modelConfig.getProvider());
            mockStream(request, modelConfig, consumer);
            return;
        }
@@ -87,6 +105,7 @@
    private void mockStream(GatewayChatRequest request, AiGatewayProperties.ModelConfig modelConfig,
                            EventConsumer consumer) throws Exception {
        long requestTime = System.currentTimeMillis();
        String modelCode = modelConfig == null ? aiGatewayProperties.getDefaultModelCode() : modelConfig.getCode();
        String lastQuestion = "";
        List<GatewayChatMessage> messages = request.getMessages();
@@ -98,22 +117,58 @@
            }
        }
        String answer = "当前为演示模式,模型[" + modelCode + "]已收到你的问题:" + lastQuestion;
        logger.info("AI gateway mock stream emitting response: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, answerLength={}",
                request.getSessionId(),
                request.getRouteCode(),
                request.getAttemptNo(),
                modelCode,
                answer.length());
        for (char c : answer.toCharArray()) {
            consumer.accept(new GatewayStreamEvent()
                    .setType("delta")
                    .setModelCode(modelCode)
                    .setContent(String.valueOf(c)));
            Thread.sleep(20L);
            try {
                Thread.sleep(20L);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return;
            }
        }
        consumer.accept(new GatewayStreamEvent()
                .setType("done")
                .setModelCode(modelCode));
                .setModelCode(modelCode)
                .setSuccess(true)
                .setRequestTime(requestTime)
                .setResponseTime(System.currentTimeMillis())
                .setDurationMs(System.currentTimeMillis() - requestTime));
        logger.info("AI gateway mock stream completed: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, durationMs={}",
                request.getSessionId(),
                request.getRouteCode(),
                request.getAttemptNo(),
                modelCode,
                System.currentTimeMillis() - requestTime);
    }
    private void openAiCompatibleStream(GatewayChatRequest request, AiGatewayProperties.ModelConfig modelConfig,
                                        EventConsumer consumer) throws Exception {
        HttpURLConnection connection = null;
        long requestTime = System.currentTimeMillis();
        boolean terminalEventSent = false;
        int eventLineCount = 0;
        int deltaCount = 0;
        int contentChars = 0;
        boolean firstDeltaLogged = false;
        String normalizedUrl = modelConfig == null ? null : modelConfig.getChatUrl();
        try {
            logger.info("AI gateway opening upstream stream: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, provider={}, url={}, modelName={}",
                    request.getSessionId(),
                    request.getRouteCode(),
                    request.getAttemptNo(),
                    modelConfig == null ? null : modelConfig.getCode(),
                    modelConfig == null ? null : modelConfig.getProvider(),
                    normalizedUrl,
                    modelConfig == null ? null : modelConfig.getModelName());
            connection = (HttpURLConnection) new URL(modelConfig.getChatUrl()).openConnection();
            connection.setConnectTimeout(aiGatewayProperties.getConnectTimeoutMillis());
            connection.setReadTimeout(aiGatewayProperties.getReadTimeoutMillis());
@@ -136,19 +191,49 @@
            }
            int statusCode = connection.getResponseCode();
            logger.info("AI gateway upstream response received: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, statusCode={}",
                    request.getSessionId(),
                    request.getRouteCode(),
                    request.getAttemptNo(),
                    modelConfig.getCode(),
                    statusCode);
            InputStream inputStream = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
            if (inputStream == null) {
                logger.warn("AI gateway upstream returned empty stream: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, url={}, statusCode={}",
                        request.getSessionId(),
                        request.getRouteCode(),
                        request.getAttemptNo(),
                        modelConfig.getCode(),
                        normalizedUrl,
                        statusCode);
                consumer.accept(new GatewayStreamEvent()
                        .setType("error")
                        .setModelCode(modelConfig.getCode())
                        .setMessage("模型服务无响应"));
                        .setMessage("模型服务无响应")
                        .setSuccess(false)
                        .setRequestTime(requestTime)
                        .setResponseTime(System.currentTimeMillis())
                        .setDurationMs(System.currentTimeMillis() - requestTime));
                terminalEventSent = true;
                return;
            }
            if (statusCode >= 400) {
                logger.warn("AI gateway upstream http error: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, url={}, statusCode={}",
                        request.getSessionId(),
                        request.getRouteCode(),
                        request.getAttemptNo(),
                        modelConfig.getCode(),
                        normalizedUrl,
                        statusCode);
                consumer.accept(new GatewayStreamEvent()
                        .setType("error")
                        .setModelCode(modelConfig.getCode())
                        .setMessage(readErrorMessage(inputStream, statusCode)));
                        .setMessage(readErrorMessage(inputStream, statusCode))
                        .setSuccess(false)
                        .setRequestTime(requestTime)
                        .setResponseTime(System.currentTimeMillis())
                        .setDurationMs(System.currentTimeMillis() - requestTime));
                terminalEventSent = true;
                return;
            }
@@ -158,11 +243,27 @@
                    if (line.trim().isEmpty() || !line.startsWith("data:")) {
                        continue;
                    }
                    eventLineCount++;
                    String payload = line.substring(5).trim();
                    if ("[DONE]".equals(payload)) {
                        long responseTime = System.currentTimeMillis();
                        logger.info("AI gateway upstream done marker received: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, eventLines={}, deltaCount={}, contentChars={}, durationMs={}",
                                request.getSessionId(),
                                request.getRouteCode(),
                                request.getAttemptNo(),
                                modelConfig.getCode(),
                                eventLineCount,
                                deltaCount,
                                contentChars,
                                responseTime - requestTime);
                        consumer.accept(new GatewayStreamEvent()
                                .setType("done")
                                .setModelCode(modelConfig.getCode()));
                                .setModelCode(modelConfig.getCode())
                                .setSuccess(true)
                                .setRequestTime(requestTime)
                                .setResponseTime(responseTime)
                                .setDurationMs(responseTime - requestTime));
                        terminalEventSent = true;
                        break;
                    }
                    JsonNode root = objectMapper.readTree(payload);
@@ -170,29 +271,117 @@
                    JsonNode delta = choice.path("delta");
                    JsonNode contentNode = delta.path("content");
                    if (!contentNode.isMissingNode() && !contentNode.isNull()) {
                        String content = contentNode.asText();
                        deltaCount++;
                        contentChars += content.length();
                        if (!firstDeltaLogged) {
                            logger.info("AI gateway upstream first delta received: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, afterMs={}, sampleLength={}",
                                    request.getSessionId(),
                                    request.getRouteCode(),
                                    request.getAttemptNo(),
                                    modelConfig.getCode(),
                                    System.currentTimeMillis() - requestTime,
                                    content.length());
                            firstDeltaLogged = true;
                        }
                        consumer.accept(new GatewayStreamEvent()
                                .setType("delta")
                                .setModelCode(modelConfig.getCode())
                                .setContent(contentNode.asText()));
                                .setContent(content));
                    }
                    JsonNode finishReason = choice.path("finish_reason");
                    if (!finishReason.isMissingNode() && !finishReason.isNull()) {
                        long responseTime = System.currentTimeMillis();
                        logger.info("AI gateway upstream finish_reason received: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, finishReason={}, eventLines={}, deltaCount={}, contentChars={}, durationMs={}",
                                request.getSessionId(),
                                request.getRouteCode(),
                                request.getAttemptNo(),
                                modelConfig.getCode(),
                                finishReason.asText(),
                                eventLineCount,
                                deltaCount,
                                contentChars,
                                responseTime - requestTime);
                        consumer.accept(new GatewayStreamEvent()
                                .setType("done")
                                .setModelCode(modelConfig.getCode()));
                                .setModelCode(modelConfig.getCode())
                                .setSuccess(true)
                                .setRequestTime(requestTime)
                                .setResponseTime(responseTime)
                                .setDurationMs(responseTime - requestTime));
                        terminalEventSent = true;
                        break;
                    }
                }
            }
            if (!terminalEventSent) {
                long responseTime = System.currentTimeMillis();
                logger.warn("AI gateway upstream ended without terminal event: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, url={}, eventLines={}, deltaCount={}, contentChars={}, durationMs={}",
                        request.getSessionId(),
                        request.getRouteCode(),
                        request.getAttemptNo(),
                        modelConfig.getCode(),
                        normalizedUrl,
                        eventLineCount,
                        deltaCount,
                        contentChars,
                        responseTime - requestTime);
                consumer.accept(new GatewayStreamEvent()
                        .setType("error")
                        .setModelCode(modelConfig.getCode())
                        .setMessage("模型流异常中断")
                        .setSuccess(false)
                        .setRequestTime(requestTime)
                        .setResponseTime(responseTime)
                        .setDurationMs(responseTime - requestTime));
            }
        } catch (Exception e) {
            if (isInterruptedError(e)) {
                logger.warn("AI gateway upstream interrupted: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, url={}, stage={}, message={}",
                        request.getSessionId(),
                        request.getRouteCode(),
                        request.getAttemptNo(),
                        modelConfig == null ? null : modelConfig.getCode(),
                        normalizedUrl,
                        terminalEventSent ? "after_terminal" : "streaming",
                        e.getMessage());
                if (e instanceof InterruptedException || e instanceof InterruptedIOException) {
                    Thread.currentThread().interrupt();
                }
                return;
            }
            logger.error("AI gateway upstream exception: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, url={}, eventLines={}, deltaCount={}, contentChars={}, message={}",
                    request.getSessionId(),
                    request.getRouteCode(),
                    request.getAttemptNo(),
                    modelConfig == null ? null : modelConfig.getCode(),
                    normalizedUrl,
                    eventLineCount,
                    deltaCount,
                    contentChars,
                    e.getMessage(),
                    e);
            consumer.accept(new GatewayStreamEvent()
                    .setType("error")
                    .setModelCode(modelConfig.getCode())
                    .setMessage(e.getMessage()));
                    .setMessage(e.getMessage())
                    .setSuccess(false)
                    .setRequestTime(requestTime)
                    .setResponseTime(System.currentTimeMillis())
                    .setDurationMs(System.currentTimeMillis() - requestTime));
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
            logger.info("AI gateway upstream stream closed: sessionId={}, routeCode={}, attemptNo={}, modelCode={}, terminalEventSent={}, eventLines={}, deltaCount={}, contentChars={}",
                    request.getSessionId(),
                    request.getRouteCode(),
                    request.getAttemptNo(),
                    modelConfig == null ? null : modelConfig.getCode(),
                    terminalEventSent,
                    eventLineCount,
                    deltaCount,
                    contentChars);
        }
    }
@@ -264,4 +453,25 @@
        }
    }
    private boolean isInterruptedError(Throwable throwable) {
        Throwable current = throwable;
        while (current != null) {
            if (current instanceof InterruptedException || current instanceof InterruptedIOException) {
                return true;
            }
            String message = current.getMessage();
            if (message != null) {
                String normalized = message.toLowerCase();
                if (normalized.contains("interrupted")
                        || normalized.contains("broken pipe")
                        || normalized.contains("connection reset")
                        || normalized.contains("forcibly closed")) {
                    return true;
                }
            }
            current = current.getCause();
        }
        return false;
    }
}
rsf-ai-gateway/src/main/java/com/vincent/rsf/ai/gateway/service/GatewayStreamEvent.java
@@ -17,4 +17,12 @@
    private String modelCode;
    private Boolean success;
    private Long requestTime;
    private Long responseTime;
    private Long durationMs;
}
rsf-ai-gateway/src/main/resources/application.yml
@@ -7,17 +7,14 @@
gateway:
  ai:
    default-model-code: mock-general
    default-model-code: deepseek-ai/DeepSeek-V3.2
    connect-timeout-millis: 10000
    read-timeout-millis: 0
    models:
      - code: mock-general
        name: Mock General
        provider: mock
        model-name: mock-general
        enabled: true
      - code: mock-creative
        name: Mock Creative
        provider: mock
        model-name: mock-creative
      - code: deepseek-ai/DeepSeek-V3.2
        name: DEEPSEEK
        provider: openai
        chat-url: https://api.siliconflow.cn
        api-key:
        model-name: deepseek-ai/DeepSeek-V3.2
        enabled: true
rsf-server/src/main/java/com/vincent/rsf/server/ServerBoot.java
@@ -11,3 +11,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiProperties.java
@@ -1,5 +1,6 @@
package com.vincent.rsf.server.ai.config;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@@ -21,7 +22,23 @@
    private String systemPrompt = "你是WMS系统内的智能助手,回答时优先保持准确、简洁,并结合上下文帮助用户理解仓储业务。";
    private String defaultModelCode = "mock-general";
    private String diagnosisSystemPrompt = "你是一名资深WMS智能诊断助手,目标是结合当前系统上下文对仓库运行情况做巡检分析。"
            + "回答时禁止凭空猜测,必须优先依据提供的实时摘要进行判断。"
            + "请优先按以下顺序分析:先总结库存、任务、设备站点的实时状态,指出是否存在明显异常;"
            + "如果发现异常,请给出异常现象、可能原因、影响范围、建议处理步骤;"
            + "如果数据正常,请明确说明当前未发现明显异常,并提醒仍需人工结合现场状态复核。"
            + "回答尽量引用你拿到的实时数据,不要编造未查询到的设备状态或业务事实。"
            + "请按“问题概述、关键证据、可能原因、建议动作、风险评估”的结构输出,并优先给出可执行建议。";
    private Integer routeFailThreshold = 3;
    private Integer routeCooldownMinutes = 10;
    private Integer diagnosticLogWindowHours = 24;
    private Integer apiFailureWindowHours = 24;
    private String defaultModelCode = "deepseek-ai/DeepSeek-V3.2";
    private List<ModelConfig> models = new ArrayList<>();
@@ -33,7 +50,18 @@
        if (defaultModelCode != null && !defaultModelCode.trim().isEmpty()) {
            return defaultModelCode;
        }
        return getEnabledModels().isEmpty() ? "mock-general" : getEnabledModels().get(0).getCode();
        return getEnabledModels().isEmpty() ? "deepseek-ai/DeepSeek-V3.2" : getEnabledModels().get(0).getCode();
    }
    public String buildScenePrompt(String sceneCode, String basePrompt) {
        String prompt = basePrompt == null ? null : basePrompt.trim();
        if (AiSceneCode.SYSTEM_DIAGNOSE.equals(sceneCode)) {
            if (prompt == null || prompt.isEmpty()) {
                return diagnosisSystemPrompt;
            }
            return prompt + "\n\n" + diagnosisSystemPrompt;
        }
        return prompt;
    }
    @Data
@@ -45,3 +73,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiSchemaGuard.java
New file
@@ -0,0 +1,64 @@
package com.vincent.rsf.server.ai.config;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.Arrays;
import java.util.List;
@Component
public class AiSchemaGuard {
    private static final List<String> REQUIRED_TABLES = Arrays.asList(
            "sys_ai_chat_session",
            "sys_ai_chat_message",
            "sys_ai_prompt_template",
            "sys_ai_prompt_publish_log",
            "sys_ai_diagnosis_record",
            "sys_ai_diagnosis_plan",
            "sys_ai_call_log",
            "sys_ai_mcp_mount",
            "sys_ai_model_route",
            "sys_ai_diagnostic_tool_config"
    );
    @Resource
    private DataSource dataSource;
    @PostConstruct
    public void validate() {
        try (Connection connection = dataSource.getConnection()) {
            for (String table : REQUIRED_TABLES) {
                if (!tableExists(connection, table)) {
                    throw new IllegalStateException("AI feature table missing: " + table + ",请先执行 AI 迁移脚本");
                }
            }
        } catch (IllegalStateException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalStateException("AI feature schema validation failed: " + e.getMessage(), e);
        }
    }
    private boolean tableExists(Connection connection, String tableName) throws Exception {
        try (ResultSet resultSet = connection.getMetaData().getTables(connection.getCatalog(), null, tableName, null)) {
            if (resultSet.next()) {
                return true;
            }
        }
        try (ResultSet resultSet = connection.getMetaData().getTables(connection.getCatalog(), null, tableName.toUpperCase(), null)) {
            if (resultSet.next()) {
                return true;
            }
        }
        try (ResultSet resultSet = connection.getMetaData().getTables(connection.getCatalog(), null, tableName.toLowerCase(), null)) {
            return resultSet.next();
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/constant/AiMcpConstants.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.server.ai.constant;
public class AiMcpConstants {
    public static final String DEFAULT_LOCAL_MOUNT_CODE = "wms_local";
    public static final String DEFAULT_LOCAL_MOUNT_NAME = "WMS本地MCP";
    public static final String TRANSPORT_AUTO = "AUTO";
    public static final String TRANSPORT_INTERNAL = "INTERNAL";
    public static final String TRANSPORT_HTTP = "HTTP";
    public static final String TRANSPORT_SSE = "SSE";
    public static final String USAGE_SCOPE_DIAGNOSE_ONLY = "DIAGNOSE_ONLY";
    public static final String USAGE_SCOPE_CHAT_AND_DIAGNOSE = "CHAT_AND_DIAGNOSE";
    public static final String USAGE_SCOPE_DISABLED = "DISABLED";
    public static final String AUTH_TYPE_NONE = "NONE";
    public static final String AUTH_TYPE_BEARER = "BEARER";
    public static final String AUTH_TYPE_API_KEY = "API_KEY";
    public static final String PROTOCOL_VERSION = "2025-03-26";
    public static final String SERVER_NAME = "wms-rsf-mcp";
    public static final String SERVER_VERSION = "1.0.0";
    private AiMcpConstants() {
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/constant/AiSceneCode.java
New file
@@ -0,0 +1,12 @@
package com.vincent.rsf.server.ai.constant;
public final class AiSceneCode {
    public static final String GENERAL_CHAT = "general_chat";
    public static final String SYSTEM_DIAGNOSE = "system_diagnose";
    private AiSceneCode() {
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiController.java
@@ -2,27 +2,32 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.ai.config.AiProperties;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.dto.AiChatStreamRequest;
import com.vincent.rsf.server.ai.dto.AiSessionCreateRequest;
import com.vincent.rsf.server.ai.dto.AiSessionRenameRequest;
import com.vincent.rsf.server.ai.dto.GatewayChatMessage;
import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
import com.vincent.rsf.server.ai.model.AiChatMessage;
import com.vincent.rsf.server.ai.model.AiChatSession;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.AiGatewayClient;
import com.vincent.rsf.server.ai.service.AiPromptContextService;
import com.vincent.rsf.server.ai.service.diagnosis.AiChatStreamOrchestrator;
import com.vincent.rsf.server.ai.service.diagnosis.AiDiagnosisRuntimeService;
import com.vincent.rsf.server.ai.service.AiModelRouteRuntimeService;
import com.vincent.rsf.server.ai.service.AiRuntimeConfigService;
import com.vincent.rsf.server.ai.service.AiSessionService;
import com.vincent.rsf.server.system.controller.BaseController;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Date;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -34,17 +39,17 @@
    @Resource
    private AiSessionService aiSessionService;
    @Resource
    private AiProperties aiProperties;
    @Resource
    private AiGatewayClient aiGatewayClient;
    @Resource
    private AiRuntimeConfigService aiRuntimeConfigService;
    @Resource
    private AiPromptContextService aiPromptContextService;
    private AiModelRouteRuntimeService aiModelRouteRuntimeService;
    @Resource
    private AiDiagnosisRuntimeService aiDiagnosisRuntimeService;
    @Resource
    private AiChatStreamOrchestrator aiChatStreamOrchestrator;
    @GetMapping("/model/list")
    public R modelList() {
        List<Map<String, Object>> models = new java.util.ArrayList<>();
        List<Map<String, Object>> models = new ArrayList<>();
        for (AiRuntimeConfigService.ModelRuntimeConfig model : aiRuntimeConfigService.listEnabledModels()) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("code", model.getCode());
@@ -99,6 +104,20 @@
    @PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter chatStream(@RequestBody AiChatStreamRequest request) {
        return doChatStream(normalizeRequest(request));
    }
    @PostMapping(value = "/diagnose/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter diagnoseStream(@RequestBody(required = false) AiChatStreamRequest request) {
        AiChatStreamRequest diagnosisRequest = normalizeRequest(request);
        diagnosisRequest.setSceneCode(AiSceneCode.SYSTEM_DIAGNOSE);
        if (diagnosisRequest.getMessage() == null || diagnosisRequest.getMessage().trim().isEmpty()) {
            diagnosisRequest.setMessage("请对当前WMS系统进行一次巡检诊断,结合库存、任务、设备站点数据识别异常并给出处理建议。");
        }
        return doChatStream(diagnosisRequest);
    }
    private SseEmitter doChatStream(AiChatStreamRequest request) {
        SseEmitter emitter = new SseEmitter(0L);
        Long tenantId = getTenantId();
        Long userId = getLoginUserId();
@@ -110,137 +129,79 @@
            completeWithError(emitter, "消息内容不能为空");
            return emitter;
        }
        AiChatSession session = aiSessionService.ensureSession(tenantId, userId, request.getSessionId(), request.getModelCode());
        aiSessionService.clearStopFlag(session.getId());
        aiSessionService.appendMessage(tenantId, userId, session.getId(), "user", request.getMessage(), session.getModelCode());
        AiRuntimeConfigService.ModelRuntimeConfig modelRuntimeConfig = aiRuntimeConfigService.resolveModel(session.getModelCode());
        AiPromptContext promptContext = new AiPromptContext()
                .setTenantId(tenantId)
                .setUserId(userId)
                .setSessionId(session.getId())
                .setModelCode(session.getModelCode())
                .setQuestion(request.getMessage())
                .setSceneCode(request.getSceneCode());
        List<AiModelRouteRuntimeService.RouteCandidate> candidates = aiModelRouteRuntimeService.resolveCandidates(
                tenantId,
                request.getSceneCode(),
                session.getModelCode()
        );
        if (candidates.isEmpty()) {
            completeWithError(emitter, "未找到可用的AI模型配置");
            return emitter;
        }
        int maxContextMessages = resolveContextSize(candidates);
        List<AiChatMessage> contextMessages = aiSessionService.listContextMessages(
                tenantId,
                userId,
                session.getId(),
                modelRuntimeConfig.getMaxContextMessages()
                maxContextMessages
        );
        AiDiagnosisRecord diagnosisRecord = AiSceneCode.SYSTEM_DIAGNOSE.equals(request.getSceneCode())
                ? aiDiagnosisRuntimeService.startDiagnosis(tenantId, userId, session.getId(), request.getSceneCode(), request.getMessage())
                : null;
        Thread thread = new Thread(() -> {
            StringBuilder assistantReply = new StringBuilder();
            boolean doneSent = false;
            try {
                emitter.send(SseEmitter.event().name("session").data(buildSessionPayload(session), MediaType.APPLICATION_JSON));
                GatewayChatRequest gatewayChatRequest = buildGatewayRequest(
                        tenantId,
                        userId,
                        session,
                        contextMessages,
                        modelRuntimeConfig,
                        request.getMessage()
                );
                aiGatewayClient.stream(gatewayChatRequest, event -> handleGatewayEvent(
                        emitter,
                        event,
                        session,
                        assistantReply
                ));
                if (aiSessionService.isStopRequested(session.getId())) {
                    if (assistantReply.length() > 0) {
                        aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), session.getModelCode());
                    }
                    emitter.send(SseEmitter.event().name("done").data(buildDonePayload(session, true), MediaType.APPLICATION_JSON));
                    doneSent = true;
                }
            } catch (Exception e) {
                try {
                    emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(e.getMessage()), MediaType.APPLICATION_JSON));
                } catch (IOException ignore) {
                }
            } finally {
                if (!doneSent && assistantReply.length() > 0) {
                    aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), session.getModelCode());
                    try {
                        emitter.send(SseEmitter.event().name("done").data(buildDonePayload(session, false), MediaType.APPLICATION_JSON));
                    } catch (IOException ignore) {
                    }
                }
                emitter.complete();
                aiSessionService.clearStopFlag(session.getId());
            }
        }, "ai-chat-stream-" + session.getId());
        Thread thread = new Thread(() -> executeStream(
                emitter, tenantId, userId, session, request, promptContext, contextMessages, diagnosisRecord, candidates
        ), "ai-chat-stream-" + session.getId());
        thread.setDaemon(true);
        thread.start();
        return emitter;
    }
    private boolean handleGatewayEvent(SseEmitter emitter, JsonNode event, AiChatSession session,
                                       StringBuilder assistantReply) throws Exception {
        if (aiSessionService.isStopRequested(session.getId())) {
            return false;
        }
        String type = event.path("type").asText();
        if ("delta".equals(type)) {
            String content = event.path("content").asText("");
            assistantReply.append(content);
            emitter.send(SseEmitter.event().name("delta").data(buildDeltaPayload(session, content), MediaType.APPLICATION_JSON));
            return true;
        }
        if ("error".equals(type)) {
            emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(event.path("message").asText("模型调用失败")), MediaType.APPLICATION_JSON));
            return false;
        }
        if ("done".equals(type)) {
            return false;
        }
        return true;
    private void executeStream(SseEmitter emitter,
                               Long tenantId,
                               Long userId,
                               AiChatSession session,
                               AiChatStreamRequest request,
                               AiPromptContext promptContext,
                               List<AiChatMessage> contextMessages,
                               AiDiagnosisRecord diagnosisRecord,
                               List<AiModelRouteRuntimeService.RouteCandidate> candidates) {
        aiChatStreamOrchestrator.executeStream(
                emitter,
                tenantId,
                userId,
                session,
                request,
                promptContext,
                contextMessages,
                diagnosisRecord,
                candidates
        );
    }
    private GatewayChatRequest buildGatewayRequest(Long tenantId, Long userId, AiChatSession session, List<AiChatMessage> contextMessages,
                                                   AiRuntimeConfigService.ModelRuntimeConfig modelRuntimeConfig,
                                                   String latestQuestion) {
        GatewayChatRequest request = new GatewayChatRequest();
        request.setSessionId(session.getId());
        request.setModelCode(session.getModelCode());
        request.setSystemPrompt(aiPromptContextService.buildSystemPrompt(
                modelRuntimeConfig.getSystemPrompt(),
                new AiPromptContext()
                        .setTenantId(tenantId)
                        .setUserId(userId)
                        .setSessionId(session.getId())
                        .setModelCode(session.getModelCode())
                        .setQuestion(latestQuestion)
        ));
        request.setChatUrl(modelRuntimeConfig.getChatUrl());
        request.setApiKey(modelRuntimeConfig.getApiKey());
        request.setModelName(modelRuntimeConfig.getModelName());
        for (AiChatMessage contextMessage : contextMessages) {
            GatewayChatMessage item = new GatewayChatMessage();
            item.setRole(contextMessage.getRole());
            item.setContent(contextMessage.getContent());
            request.getMessages().add(item);
    private int resolveContextSize(List<AiModelRouteRuntimeService.RouteCandidate> candidates) {
        return aiChatStreamOrchestrator.resolveContextSize(candidates);
    }
    private AiChatStreamRequest normalizeRequest(AiChatStreamRequest request) {
        AiChatStreamRequest normalized = request == null ? new AiChatStreamRequest() : request;
        if (normalized.getSceneCode() == null || normalized.getSceneCode().trim().isEmpty()) {
            normalized.setSceneCode(AiSceneCode.GENERAL_CHAT);
        }
        return request;
    }
    private Map<String, Object> buildSessionPayload(AiChatSession session) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sessionId", session.getId());
        payload.put("title", session.getTitle());
        payload.put("modelCode", session.getModelCode());
        return payload;
    }
    private Map<String, Object> buildDeltaPayload(AiChatSession session, String content) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sessionId", session.getId());
        payload.put("modelCode", session.getModelCode());
        payload.put("content", content);
        payload.put("timestamp", new Date().getTime());
        return payload;
    }
    private Map<String, Object> buildDonePayload(AiChatSession session, boolean stopped) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sessionId", session.getId());
        payload.put("modelCode", session.getModelCode());
        payload.put("stopped", stopped);
        return payload;
        return normalized;
    }
    private Map<String, Object> buildErrorPayload(String message) {
@@ -256,5 +217,5 @@
        }
        emitter.complete();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatStreamRequest.java
@@ -13,4 +13,7 @@
    private String modelCode;
    private String sceneCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionCreateRequest.java
@@ -12,3 +12,4 @@
    private String modelCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiSessionRenameRequest.java
@@ -10,3 +10,4 @@
    private String title;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatMessage.java
@@ -12,3 +12,4 @@
    private String content;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/GatewayChatRequest.java
@@ -13,6 +13,10 @@
    private String modelCode;
    private String routeCode;
    private Integer attemptNo;
    private String systemPrompt;
    private String chatUrl;
@@ -24,3 +28,4 @@
    private List<GatewayChatMessage> messages = new ArrayList<>();
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/mapper/AiChatMessageMapper.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.ai.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.ai.model.AiChatMessage;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiChatMessageMapper extends BaseMapper<AiChatMessage> {
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/mapper/AiChatSessionMapper.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.ai.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.ai.model.AiChatSession;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiChatSessionMapper extends BaseMapper<AiChatSession> {
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatMessage.java
@@ -1,5 +1,9 @@
package com.vincent.rsf.server.ai.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -8,9 +12,15 @@
@Data
@Accessors(chain = true)
@TableName("sys_ai_chat_message")
public class AiChatMessage implements Serializable {
    @TableId(value = "id", type = IdType.INPUT)
    private String id;
    private Long tenantId;
    private Long userId;
    private String sessionId;
@@ -22,4 +32,10 @@
    private Date createTime;
    private Integer status;
    @TableLogic
    private Integer deleted;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiChatSession.java
@@ -1,5 +1,9 @@
package com.vincent.rsf.server.ai.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -8,9 +12,15 @@
@Data
@Accessors(chain = true)
@TableName("sys_ai_chat_session")
public class AiChatSession implements Serializable {
    @TableId(value = "id", type = IdType.INPUT)
    private String id;
    private Long tenantId;
    private Long userId;
    private String title;
@@ -24,4 +34,10 @@
    private Date updateTime;
    private Integer status;
    @TableLogic
    private Integer deleted;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiDiagnosticToolResult.java
New file
@@ -0,0 +1,28 @@
package com.vincent.rsf.server.ai.model;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Map;
@Data
@Accessors(chain = true)
public class AiDiagnosticToolResult implements Serializable {
    private String toolCode;
    private String mountCode;
    private String mcpToolName;
    private String toolName;
    private String severity;
    private String summaryText;
    private Map<String, Object> rawMeta;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiMcpToolDescriptor.java
New file
@@ -0,0 +1,39 @@
package com.vincent.rsf.server.ai.model;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Map;
@Data
@Accessors(chain = true)
public class AiMcpToolDescriptor implements Serializable {
    private String mountCode;
    private String mountName;
    private String toolCode;
    private String mcpToolName;
    private String toolName;
    private String sceneCode;
    private String description;
    private Integer enabledFlag;
    private Integer priority;
    private String toolPrompt;
    private String usageScope;
    private String transportType;
    private Map<String, Object> inputSchema;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/model/AiPromptContext.java
@@ -18,4 +18,7 @@
    private String modelCode;
    private String question;
    private String sceneCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiGatewayClient.java
@@ -8,6 +8,7 @@
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
@@ -24,11 +25,20 @@
    private ObjectMapper objectMapper;
    public interface StreamCallback {
        /**
         * 处理网关返回的一条 NDJSON 事件。
         * 返回 true 表示继续消费后续事件,返回 false 表示主动停止本次流读取。
         */
        boolean handle(JsonNode event) throws Exception;
    }
    /**
     * 调用 AI 网关的内部流式接口,并将网关返回的事件逐条回调给上层编排逻辑。
     * 这里屏蔽了 HTTP 细节,调用方只需要关注 delta / done / error 事件本身。
     */
    public void stream(GatewayChatRequest request, StreamCallback callback) throws Exception {
        HttpURLConnection connection = null;
        boolean terminalEventReceived = false;
        try {
            String url = aiProperties.getGatewayBaseUrl() + "/internal/chat/stream";
            connection = (HttpURLConnection) new URL(url).openConnection();
@@ -53,10 +63,17 @@
                        continue;
                    }
                    JsonNode event = objectMapper.readTree(line);
                    String type = event.path("type").asText();
                    if ("done".equals(type) || "error".equals(type)) {
                        terminalEventReceived = true;
                    }
                    if (!callback.handle(event)) {
                        break;
                    }
                }
            }
            if (!terminalEventReceived) {
                throw new IOException("AI网关流异常中断");
            }
        } finally {
            if (connection != null) {
@@ -66,3 +83,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiModelRouteRuntimeService.java
New file
@@ -0,0 +1,145 @@
package com.vincent.rsf.server.ai.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.system.entity.AiModelRoute;
import com.vincent.rsf.server.system.service.AiModelRouteService;
import lombok.Data;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@Service
public class AiModelRouteRuntimeService {
    @Resource
    private AiRuntimeConfigService aiRuntimeConfigService;
    @Resource
    private AiModelRouteService aiModelRouteService;
    @Resource
    private com.vincent.rsf.server.ai.config.AiProperties aiProperties;
    /**
     * 为一次聊天/诊断请求解析候选模型列表。
     * 顺序是:会话首选模型 -> 当前场景可用路由 -> 默认模型。
     */
    public List<RouteCandidate> resolveCandidates(Long tenantId, String sceneCode, String preferredModelCode) {
        List<RouteCandidate> output = new ArrayList<>();
        Set<String> seen = new LinkedHashSet<>();
        if (preferredModelCode != null && !preferredModelCode.trim().isEmpty()) {
            RouteCandidate preferredCandidate = toCandidate(null, preferredModelCode, seen);
            if (preferredCandidate != null) {
                output.add(preferredCandidate);
            }
        }
        String routeCode = resolveRouteCode(sceneCode);
        for (AiModelRoute route : aiModelRouteService.listAvailableRoutes(tenantId, routeCode)) {
            RouteCandidate candidate = toCandidate(route, route.getModelCode(), seen);
            if (candidate != null) {
                output.add(candidate);
            }
        }
        if (output.isEmpty()) {
            RouteCandidate defaultCandidate = toCandidate(null, aiRuntimeConfigService.resolveDefaultModelCode(), seen);
            if (defaultCandidate != null) {
                output.add(defaultCandidate);
            }
        }
        return output;
    }
    /**
     * 标记某条模型路由本次调用成功,并清空失败计数和冷却状态。
     */
    public void markSuccess(Long routeId) {
        if (routeId == null) {
            return;
        }
        aiModelRouteService.update(new LambdaUpdateWrapper<AiModelRoute>()
                .setSql("success_count = IFNULL(success_count, 0) + 1")
                .set(AiModelRoute::getFailCount, 0)
                .set(AiModelRoute::getCooldownUntil, null)
                .eq(AiModelRoute::getId, routeId));
    }
    /**
     * 标记某条模型路由本次调用失败。
     * 连续失败达到阈值后会进入冷却期,避免短时间内持续命中故障模型。
     */
    public void markFailure(Long routeId) {
        if (routeId == null) {
            return;
        }
        AiModelRoute route = aiModelRouteService.getById(routeId);
        if (route == null) {
            return;
        }
        int nextFailCount = (route.getFailCount() == null ? 0 : route.getFailCount()) + 1;
        Date cooldownUntil = route.getCooldownUntil();
        if (nextFailCount >= aiProperties.getRouteFailThreshold()) {
            cooldownUntil = new Date(System.currentTimeMillis() + aiProperties.getRouteCooldownMinutes() * 60_000L);
        }
        route.setFailCount(nextFailCount);
        route.setCooldownUntil(cooldownUntil);
        aiModelRouteService.updateById(route);
    }
    /**
     * 手动重置一条路由的成功/失败统计和冷却时间。
     */
    public void resetRoute(Long routeId) {
        if (routeId == null) {
            return;
        }
        aiModelRouteService.update(new LambdaUpdateWrapper<AiModelRoute>()
                .set(AiModelRoute::getFailCount, 0)
                .set(AiModelRoute::getSuccessCount, 0)
                .set(AiModelRoute::getCooldownUntil, null)
                .eq(AiModelRoute::getId, routeId));
    }
    /**
     * 将聊天场景统一映射成路由组编码。
     */
    private String resolveRouteCode(String sceneCode) {
        if (AiSceneCode.SYSTEM_DIAGNOSE.equals(sceneCode)) {
            return AiSceneCode.SYSTEM_DIAGNOSE;
        }
        return AiSceneCode.GENERAL_CHAT;
    }
    /**
     * 基于模型编码和路由记录组装运行时候选项,并负责去重与可用性校验。
     */
    private RouteCandidate toCandidate(AiModelRoute route, String modelCode, Set<String> seen) {
        if (modelCode == null || modelCode.trim().isEmpty() || seen.contains(modelCode)) {
            return null;
        }
        AiRuntimeConfigService.ModelRuntimeConfig runtimeConfig = aiRuntimeConfigService.resolveModel(modelCode);
        if (runtimeConfig == null || !Boolean.TRUE.equals(runtimeConfig.getEnabled())) {
            return null;
        }
        seen.add(modelCode);
        RouteCandidate candidate = new RouteCandidate();
        candidate.setRouteId(route == null ? null : route.getId());
        candidate.setRouteCode(route == null ? null : route.getRouteCode());
        candidate.setAttemptModelCode(runtimeConfig.getCode());
        candidate.setRuntimeConfig(runtimeConfig);
        return candidate;
    }
    @Data
    public static class RouteCandidate {
        private Long routeId;
        private String routeCode;
        private String attemptModelCode;
        private AiRuntimeConfigService.ModelRuntimeConfig runtimeConfig;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextProvider.java
@@ -4,7 +4,14 @@
public interface AiPromptContextProvider {
    /**
     * 判断当前上下文是否需要由该提供器补充系统提示词。
     */
    boolean supports(AiPromptContext context);
    /**
     * 根据当前请求上下文生成一段可附加到系统 Prompt 的业务背景描述。
     */
    String buildContext(AiPromptContext context);
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptContextService.java
@@ -15,6 +15,10 @@
        this.providers = providers == null ? new ArrayList<>() : providers;
    }
    /**
     * 将基础 Prompt 与所有命中的上下文提供器结果拼装成最终系统提示词。
     * 普通聊天场景主要依赖这条链补充业务背景,诊断场景则在此基础上叠加工具摘要。
     */
    public String buildSystemPrompt(String basePrompt, AiPromptContext context) {
        List<String> promptParts = new ArrayList<>();
        if (basePrompt != null && !basePrompt.trim().isEmpty()) {
@@ -35,3 +39,4 @@
        return String.join("\n\n", promptParts);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiPromptRuntimeService.java
New file
@@ -0,0 +1,87 @@
package com.vincent.rsf.server.ai.service;
import com.vincent.rsf.server.ai.config.AiProperties;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.diagnosis.AiDiagnosticToolService;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import com.vincent.rsf.server.system.service.AiPromptTemplateService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@Service
public class AiPromptRuntimeService {
    @Resource
    private AiProperties aiProperties;
    @Resource
    private AiPromptContextService aiPromptContextService;
    @Resource
    private AiPromptTemplateService aiPromptTemplateService;
    @Resource
    private AiDiagnosticToolService aiDiagnosticToolService;
    /**
     * 构造指定场景的系统 Prompt。
     * 当未显式传入诊断工具结果时,诊断场景会在内部主动收集一次工具摘要。
     */
    public String buildSystemPrompt(String sceneCode, String fallbackBasePrompt, AiPromptContext context) {
        return buildSystemPrompt(sceneCode, fallbackBasePrompt, context, null);
    }
    /**
     * 构造最终系统 Prompt,按“已发布模板/默认模板 + 上下文 + 工具摘要”的顺序拼装。
     */
    public String buildSystemPrompt(String sceneCode, String fallbackBasePrompt, AiPromptContext context,
                                    List<AiDiagnosticToolResult> diagnosticResults) {
        String basePrompt = resolveBasePrompt(sceneCode, fallbackBasePrompt, context.getTenantId());
        List<AiDiagnosticToolResult> results = diagnosticResults;
        if (results == null && AiSceneCode.SYSTEM_DIAGNOSE.equals(sceneCode)) {
            results = aiDiagnosticToolService.collect(context);
        }
        if (results != null && !results.isEmpty()) {
            List<String> parts = new ArrayList<>();
            String contextPrompt = aiPromptContextService.buildSystemPrompt(basePrompt, context);
            if (contextPrompt != null && !contextPrompt.trim().isEmpty()) {
                parts.add(contextPrompt.trim());
            }
            String diagnosticPrompt = aiDiagnosticToolService.buildPrompt(context.getTenantId(), sceneCode, results);
            if (!diagnosticPrompt.trim().isEmpty()) {
                parts.add(diagnosticPrompt);
            }
            return String.join("\n\n", parts);
        }
        return aiPromptContextService.buildSystemPrompt(basePrompt, context);
    }
    /**
     * 解析当前场景的基础 Prompt。
     * 优先使用已发布的 Prompt 模板;若模板不存在或为空,则回退到配置文件中的默认提示词。
     */
    private String resolveBasePrompt(String sceneCode, String fallbackBasePrompt, Long tenantId) {
        AiPromptTemplate publishedTemplate = aiPromptTemplateService.getPublishedTemplate(tenantId, sceneCode);
        if (publishedTemplate == null) {
            return aiProperties.buildScenePrompt(sceneCode, fallbackBasePrompt);
        }
        List<String> parts = new ArrayList<>();
        if (publishedTemplate.getBasePrompt() != null && !publishedTemplate.getBasePrompt().trim().isEmpty()) {
            parts.add(publishedTemplate.getBasePrompt().trim());
        }
        if (publishedTemplate.getToolPrompt() != null && !publishedTemplate.getToolPrompt().trim().isEmpty()) {
            parts.add(publishedTemplate.getToolPrompt().trim());
        }
        if (publishedTemplate.getOutputPrompt() != null && !publishedTemplate.getOutputPrompt().trim().isEmpty()) {
            parts.add(publishedTemplate.getOutputPrompt().trim());
        }
        if (parts.isEmpty()) {
            return aiProperties.buildScenePrompt(sceneCode, fallbackBasePrompt);
        }
        return String.join("\n\n", parts);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiRuntimeConfigService.java
@@ -18,6 +18,10 @@
    @Resource
    private AiParamService aiParamService;
    /**
     * 枚举当前可用的模型运行时配置。
     * 优先从数据库读取租户可运营的模型参数;数据库不可用时回退到 application 配置。
     */
    public List<ModelRuntimeConfig> listEnabledModels() {
        List<ModelRuntimeConfig> output = new ArrayList<>();
        try {
@@ -43,6 +47,10 @@
        return output;
    }
    /**
     * 解析指定模型编码对应的运行时配置。
     * 如果未指定模型编码,则返回当前默认模型;如果数据库无记录,则回退到静态配置。
     */
    public ModelRuntimeConfig resolveModel(String modelCode) {
        try {
            AiParam aiParam;
@@ -82,10 +90,16 @@
        return config;
    }
    /**
     * 获取系统当前默认模型编码。
     */
    public String resolveDefaultModelCode() {
        return resolveModel(null).getCode();
    }
    /**
     * 将数据库中的 AI 参数实体转换为运行时统一使用的模型配置对象。
     */
    private ModelRuntimeConfig toRuntimeConfig(AiParam aiParam) {
        ModelRuntimeConfig config = new ModelRuntimeConfig();
        config.setCode(aiParam.getModelCode());
@@ -117,3 +131,4 @@
        private Boolean enabled;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiSessionService.java
@@ -7,28 +7,65 @@
public interface AiSessionService {
    /**
     * 查询当前用户可见的 AI 会话列表,按最近更新时间倒序返回。
     */
    List<AiChatSession> listSessions(Long tenantId, Long userId);
    /**
     * 显式创建一个新会话,并根据传入模型或默认模型初始化会话元数据。
     */
    AiChatSession createSession(Long tenantId, Long userId, String title, String modelCode);
    /**
     * 确保指定会话存在;如果会话不存在则自动创建,存在时可顺带更新模型偏好。
     */
    AiChatSession ensureSession(Long tenantId, Long userId, String sessionId, String modelCode);
    /**
     * 按租户、用户和会话 ID 精确读取会话,避免跨租户/跨用户串会话。
     */
    AiChatSession getSession(Long tenantId, Long userId, String sessionId);
    /**
     * 重命名会话标题。
     */
    AiChatSession renameSession(Long tenantId, Long userId, String sessionId, String title);
    /**
     * 删除会话及其聊天消息。
     */
    void removeSession(Long tenantId, Long userId, String sessionId);
    /**
     * 查询会话下的完整消息列表。
     */
    List<AiChatMessage> listMessages(Long tenantId, Long userId, String sessionId);
    /**
     * 查询构造上下文所需的最近若干条消息。
     */
    List<AiChatMessage> listContextMessages(Long tenantId, Long userId, String sessionId, int maxCount);
    /**
     * 追加一条聊天消息,并同步刷新会话最后消息、最后活跃时间和模型信息。
     */
    AiChatMessage appendMessage(Long tenantId, Long userId, String sessionId, String role, String content, String modelCode);
    /**
     * 清除会话的“停止生成”标记,通常在一次流式对话收尾时调用。
     */
    void clearStopFlag(String sessionId);
    /**
     * 标记会话需要停止生成,供流式编排线程轮询消费。
     */
    void requestStop(String sessionId);
    /**
     * 判断当前会话是否已收到停止生成请求。
     */
    boolean isStopRequested(String sessionId);
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTextCompletionService.java
New file
@@ -0,0 +1,40 @@
package com.vincent.rsf.server.ai.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class AiTextCompletionService {
    @Resource
    private AiGatewayClient aiGatewayClient;
    /**
     * 用流式网关接口模拟一次“同步补全文本”调用。
     * 适合工具规划、结构化 JSON 选择这类短文本任务,内部仍然复用统一的流式网关能力。
     */
    public String complete(GatewayChatRequest request) throws Exception {
        final StringBuilder output = new StringBuilder();
        aiGatewayClient.stream(request, event -> handleEvent(event, output));
        return output.toString().trim();
    }
    /**
     * 只收集 delta 文本,并把 error / done 事件转换成同步调用语义。
     */
    private boolean handleEvent(JsonNode event, StringBuilder output) {
        String type = event.path("type").asText("");
        if ("delta".equals(type)) {
            output.append(event.path("content").asText(""));
            return true;
        }
        if ("error".equals(type)) {
            throw new IllegalStateException(event.path("message").asText("模型调用失败"));
        }
        return !"done".equals(type);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiChatStreamOrchestrator.java
New file
@@ -0,0 +1,467 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.fasterxml.jackson.databind.JsonNode;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.dto.AiChatStreamRequest;
import com.vincent.rsf.server.ai.dto.GatewayChatMessage;
import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
import com.vincent.rsf.server.ai.model.AiChatMessage;
import com.vincent.rsf.server.ai.model.AiChatSession;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.AiGatewayClient;
import com.vincent.rsf.server.ai.service.AiModelRouteRuntimeService;
import com.vincent.rsf.server.ai.service.AiPromptRuntimeService;
import com.vincent.rsf.server.ai.service.AiSessionService;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiChatStreamOrchestrator {
    @Resource
    private AiSessionService aiSessionService;
    @Resource
    private AiGatewayClient aiGatewayClient;
    @Resource
    private AiPromptRuntimeService aiPromptRuntimeService;
    @Resource
    private AiDiagnosticToolService aiDiagnosticToolService;
    @Resource
    private AiModelRouteRuntimeService aiModelRouteRuntimeService;
    @Resource
    private AiDiagnosisRuntimeService aiDiagnosisRuntimeService;
    @Resource
    private AiDiagnosisMcpRuntimeService aiDiagnosisMcpRuntimeService;
    /**
     * 从候选模型列表中挑出最大的上下文窗口,用于提前截断会话历史。
     */
    public int resolveContextSize(List<AiModelRouteRuntimeService.RouteCandidate> candidates) {
        int max = 12;
        for (AiModelRouteRuntimeService.RouteCandidate item : candidates) {
            if (item != null && item.getRuntimeConfig() != null
                    && item.getRuntimeConfig().getMaxContextMessages() != null
                    && item.getRuntimeConfig().getMaxContextMessages() > max) {
                max = item.getRuntimeConfig().getMaxContextMessages();
            }
        }
        return max;
    }
    /**
     * 执行一次完整的流式聊天/诊断编排。
     * 这里统一负责 MCP 结果准备、模型重试、事件转发、调用日志和诊断收尾。
     */
    public void executeStream(SseEmitter emitter,
                              Long tenantId,
                              Long userId,
                              AiChatSession session,
                              AiChatStreamRequest request,
                              AiPromptContext promptContext,
                              List<AiChatMessage> contextMessages,
                              AiDiagnosisRecord diagnosisRecord,
                              List<AiModelRouteRuntimeService.RouteCandidate> candidates) {
        StringBuilder assistantReply = new StringBuilder();
        String finalModelCode = session.getModelCode();
        String finalErrorMessage = null;
        boolean stopped = false;
        boolean success = false;
        boolean assistantSaved = false;
        boolean errorSent = false;
        List<AiDiagnosticToolResult> runtimeDiagnosticResults = new ArrayList<>();
        String toolSummary = "[]";
        try {
            emitter.send(SseEmitter.event().name("session").data(buildSessionPayload(session), MediaType.APPLICATION_JSON));
            if (aiDiagnosisMcpRuntimeService.shouldUseMcp(promptContext)) {
                runtimeDiagnosticResults = aiDiagnosisMcpRuntimeService.resolveToolResults(
                        tenantId,
                        promptContext,
                        contextMessages,
                        candidates.isEmpty() ? null : candidates.get(0)
                );
                toolSummary = aiDiagnosticToolService.serializeResults(runtimeDiagnosticResults);
            }
            int attemptNo = 1;
            for (AiModelRouteRuntimeService.RouteCandidate candidate : candidates) {
                AttemptState attemptState = new AttemptState();
                Date requestTime = new Date();
                try {
                    GatewayChatRequest gatewayChatRequest = buildGatewayRequest(
                            session,
                            contextMessages,
                            candidate,
                            request,
                            promptContext,
                            runtimeDiagnosticResults,
                            attemptNo
                    );
                    aiGatewayClient.stream(gatewayChatRequest, event -> handleGatewayEvent(
                            emitter,
                            event,
                            session,
                            assistantReply,
                            attemptState
                    ));
                } catch (Exception e) {
                    attemptState.setSuccess(false);
                    attemptState.setErrorMessage(e.getMessage());
                    attemptState.setInterrupted(isInterruptedError(e));
                    attemptState.setResponseTime(new Date());
                }
                if (attemptState.getResponseTime() == null) {
                    attemptState.setResponseTime(new Date());
                }
                String actualModelCode = attemptState.getActualModelCode() == null
                        ? candidate.getAttemptModelCode()
                        : attemptState.getActualModelCode();
                finalModelCode = actualModelCode;
                aiDiagnosisRuntimeService.saveCallLog(
                        tenantId,
                        userId,
                        session.getId(),
                        diagnosisRecord == null ? null : diagnosisRecord.getId(),
                        resolveRouteCode(candidate, request),
                        actualModelCode,
                        attemptNo,
                        requestTime,
                        attemptState.getResponseTime(),
                        Boolean.TRUE.equals(attemptState.getSuccess()) ? 1 : 0,
                        attemptState.getErrorMessage()
                );
                if (Boolean.TRUE.equals(attemptState.getSuccess())) {
                    aiModelRouteRuntimeService.markSuccess(candidate.getRouteId());
                    success = assistantReply.length() > 0;
                    if (!success) {
                        finalErrorMessage = "模型未返回有效内容";
                    }
                    break;
                }
                if (attemptState.isStopped() || aiSessionService.isStopRequested(session.getId())) {
                    stopped = true;
                    break;
                }
                if (!attemptState.isInterrupted()) {
                    aiModelRouteRuntimeService.markFailure(candidate.getRouteId());
                }
                finalErrorMessage = attemptState.getErrorMessage();
                if (attemptState.isReceivedDelta() || attemptNo >= candidates.size()) {
                    if (!attemptState.isInterrupted() && finalErrorMessage != null && !finalErrorMessage.trim().isEmpty()) {
                        emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(finalErrorMessage), MediaType.APPLICATION_JSON));
                        errorSent = true;
                    }
                    break;
                }
                attemptNo++;
            }
            if (aiSessionService.isStopRequested(session.getId())) {
                stopped = true;
            }
            if (stopped) {
                if (assistantReply.length() > 0) {
                    aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), finalModelCode);
                    assistantSaved = true;
                }
                emitter.send(SseEmitter.event().name("done").data(buildDonePayload(session, finalModelCode, true), MediaType.APPLICATION_JSON));
                if (diagnosisRecord != null) {
                    aiDiagnosisRuntimeService.finishDiagnosisFailure(diagnosisRecord, assistantReply.toString(), "用户已停止生成", toolSummary);
                }
                return;
            }
            if (success) {
                aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), finalModelCode);
                assistantSaved = true;
                emitter.send(SseEmitter.event().name("done").data(buildDonePayload(session, finalModelCode, false), MediaType.APPLICATION_JSON));
                if (diagnosisRecord != null) {
                    aiDiagnosisRuntimeService.finishDiagnosisSuccess(diagnosisRecord, assistantReply.toString(), finalModelCode, toolSummary);
                }
                return;
            }
            if (assistantReply.length() > 0 && !assistantSaved) {
                aiSessionService.appendMessage(tenantId, userId, session.getId(), "assistant", assistantReply.toString(), finalModelCode);
                assistantSaved = true;
            }
            if (diagnosisRecord != null) {
                aiDiagnosisRuntimeService.finishDiagnosisFailure(diagnosisRecord, assistantReply.toString(), finalErrorMessage, toolSummary);
            }
            if (!errorSent && finalErrorMessage != null && !finalErrorMessage.trim().isEmpty()) {
                emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(finalErrorMessage), MediaType.APPLICATION_JSON));
            }
        } catch (Exception e) {
            if (diagnosisRecord != null) {
                aiDiagnosisRuntimeService.finishDiagnosisFailure(diagnosisRecord, assistantReply.toString(), e.getMessage(), toolSummary);
            }
            if (!isInterruptedError(e)) {
                try {
                    emitter.send(SseEmitter.event().name("error").data(buildErrorPayload(e.getMessage()), MediaType.APPLICATION_JSON));
                } catch (IOException ignore) {
                }
            } else {
                Thread.currentThread().interrupt();
            }
        } finally {
            emitter.complete();
            aiSessionService.clearStopFlag(session.getId());
        }
    }
    /**
     * 消费网关返回的单条流式事件,并把状态写回本次尝试上下文。
     */
    private boolean handleGatewayEvent(SseEmitter emitter, JsonNode event, AiChatSession session,
                                       StringBuilder assistantReply, AttemptState attemptState) throws Exception {
        if (aiSessionService.isStopRequested(session.getId())) {
            attemptState.setStopped(true);
            attemptState.setResponseTime(new Date());
            return false;
        }
        String type = event.path("type").asText();
        String modelCode = event.path("modelCode").asText(session.getModelCode());
        if ("delta".equals(type)) {
            String content = event.path("content").asText("");
            assistantReply.append(content);
            attemptState.setReceivedDelta(true);
            attemptState.setActualModelCode(modelCode);
            emitter.send(SseEmitter.event().name("delta").data(buildDeltaPayload(session, modelCode, content), MediaType.APPLICATION_JSON));
            return true;
        }
        if ("error".equals(type)) {
            String message = event.path("message").asText("模型调用失败");
            attemptState.setSuccess(false);
            attemptState.setErrorMessage(message);
            attemptState.setActualModelCode(modelCode);
            attemptState.setResponseTime(parseResponseTime(event));
            attemptState.setInterrupted(isInterruptedMessage(message));
            return false;
        }
        if ("done".equals(type)) {
            attemptState.setSuccess(true);
            attemptState.setActualModelCode(modelCode);
            attemptState.setResponseTime(parseResponseTime(event));
            return false;
        }
        if ("ping".equals(type)) {
            emitter.send(SseEmitter.event().name("ping").data(buildPingPayload(modelCode), MediaType.APPLICATION_JSON));
            return true;
        }
        return true;
    }
    /**
     * 将当前尝试需要的上下文、系统 Prompt 和模型信息组装成网关请求。
     */
    private GatewayChatRequest buildGatewayRequest(AiChatSession session,
                                                   List<AiChatMessage> contextMessages,
                                                   AiModelRouteRuntimeService.RouteCandidate candidate,
                                                   AiChatStreamRequest chatRequest,
                                                   AiPromptContext promptContext,
                                                   List<AiDiagnosticToolResult> diagnosticResults,
                                                   Integer attemptNo) {
        GatewayChatRequest request = new GatewayChatRequest();
        request.setSessionId(session.getId());
        request.setModelCode(candidate.getAttemptModelCode());
        request.setRouteCode(resolveRouteCode(candidate, chatRequest));
        request.setAttemptNo(attemptNo);
        request.setSystemPrompt(aiPromptRuntimeService.buildSystemPrompt(
                chatRequest.getSceneCode(),
                candidate.getRuntimeConfig().getSystemPrompt(),
                promptContext,
                diagnosticResults
        ));
        request.setChatUrl(candidate.getRuntimeConfig().getChatUrl());
        request.setApiKey(candidate.getRuntimeConfig().getApiKey());
        request.setModelName(candidate.getRuntimeConfig().getModelName());
        for (AiChatMessage contextMessage : contextMessages) {
            GatewayChatMessage item = new GatewayChatMessage();
            item.setRole(contextMessage.getRole());
            item.setContent(contextMessage.getContent());
            request.getMessages().add(item);
        }
        return request;
    }
    /**
     * 解析当前尝试应落到哪个路由编码,优先使用路由候选自带的 routeCode。
     */
    private String resolveRouteCode(AiModelRouteRuntimeService.RouteCandidate candidate, AiChatStreamRequest request) {
        if (candidate != null && candidate.getRouteCode() != null && !candidate.getRouteCode().trim().isEmpty()) {
            return candidate.getRouteCode();
        }
        return AiSceneCode.SYSTEM_DIAGNOSE.equals(request.getSceneCode())
                ? AiSceneCode.SYSTEM_DIAGNOSE
                : AiSceneCode.GENERAL_CHAT;
    }
    /**
     * 从网关事件中解析响应时间,缺省时回退为当前时间。
     */
    private Date parseResponseTime(JsonNode event) {
        long millis = event.path("responseTime").asLong(0L);
        return millis <= 0L ? new Date() : new Date(millis);
    }
    /**
     * 构造会话初始化事件给前端。
     */
    private Map<String, Object> buildSessionPayload(AiChatSession session) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sessionId", session.getId());
        payload.put("title", session.getTitle());
        payload.put("modelCode", session.getModelCode());
        return payload;
    }
    /**
     * 构造增量输出事件给前端。
     */
    private Map<String, Object> buildDeltaPayload(AiChatSession session, String modelCode, String content) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sessionId", session.getId());
        payload.put("modelCode", modelCode);
        payload.put("content", content);
        payload.put("timestamp", new Date().getTime());
        return payload;
    }
    /**
     * 构造流式完成事件给前端。
     */
    private Map<String, Object> buildDonePayload(AiChatSession session, String modelCode, boolean stopped) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("sessionId", session.getId());
        payload.put("modelCode", modelCode);
        payload.put("stopped", stopped);
        return payload;
    }
    /**
     * 构造错误事件给前端。
     */
    private Map<String, Object> buildErrorPayload(String message) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("message", message == null ? "AI服务异常" : message);
        return payload;
    }
    /**
     * 构造心跳事件给前端,用于维持长连接活性。
     */
    private Map<String, Object> buildPingPayload(String modelCode) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("modelCode", modelCode);
        payload.put("timestamp", new Date().getTime());
        return payload;
    }
    /**
     * 判断异常链中是否包含线程中断类错误。
     */
    private boolean isInterruptedError(Throwable throwable) {
        Throwable current = throwable;
        while (current != null) {
            if (current instanceof InterruptedException || current instanceof InterruptedIOException) {
                return true;
            }
            if (isInterruptedMessage(current.getMessage())) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }
    private boolean isInterruptedMessage(String message) {
        if (message == null || message.trim().isEmpty()) {
            return false;
        }
        String normalized = message.toLowerCase();
        return normalized.contains("interrupted")
                || normalized.contains("broken pipe")
                || normalized.contains("connection reset")
                || normalized.contains("forcibly closed");
    }
    private static class AttemptState {
        private Boolean success;
        private String actualModelCode;
        private String errorMessage;
        private boolean receivedDelta;
        private boolean interrupted;
        private boolean stopped;
        private Date responseTime;
        public Boolean getSuccess() {
            return success;
        }
        public void setSuccess(Boolean success) {
            this.success = success;
        }
        public String getActualModelCode() {
            return actualModelCode;
        }
        public void setActualModelCode(String actualModelCode) {
            this.actualModelCode = actualModelCode;
        }
        public String getErrorMessage() {
            return errorMessage;
        }
        public void setErrorMessage(String errorMessage) {
            this.errorMessage = errorMessage;
        }
        public boolean isReceivedDelta() {
            return receivedDelta;
        }
        public void setReceivedDelta(boolean receivedDelta) {
            this.receivedDelta = receivedDelta;
        }
        public boolean isInterrupted() {
            return interrupted;
        }
        public void setInterrupted(boolean interrupted) {
            this.interrupted = interrupted;
        }
        public boolean isStopped() {
            return stopped;
        }
        public void setStopped(boolean stopped) {
            this.stopped = stopped;
        }
        public Date getResponseTime() {
            return responseTime;
        }
        public void setResponseTime(Date responseTime) {
            this.responseTime = responseTime;
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisMcpRuntimeService.java
New file
@@ -0,0 +1,426 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.dto.GatewayChatMessage;
import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
import com.vincent.rsf.server.ai.model.AiChatMessage;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.AiModelRouteRuntimeService;
import com.vincent.rsf.server.ai.service.AiTextCompletionService;
import com.vincent.rsf.server.ai.service.mcp.AiMcpRegistryService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
public class AiDiagnosisMcpRuntimeService {
    @Resource
    private AiMcpRegistryService aiMcpRegistryService;
    @Resource
    private AiDiagnosticToolService aiDiagnosticToolService;
    @Resource
    private AiTextCompletionService aiTextCompletionService;
    @Resource
    private ObjectMapper objectMapper;
    /**
     * 根据当前问题、上下文消息和可用模型,解析出本轮真正要执行的 MCP 工具结果。
     * 先做工具过滤,再做启发式或模型规划选择,最后顺序执行工具。
     */
    public List<AiDiagnosticToolResult> resolveToolResults(Long tenantId,
                                                           AiPromptContext context,
                                                           List<AiChatMessage> contextMessages,
                                                           AiModelRouteRuntimeService.RouteCandidate plannerCandidate) {
        List<AiMcpToolDescriptor> tools = filterTools(aiMcpRegistryService.listTools(tenantId, null), context.getSceneCode());
        if (tools.isEmpty()) {
            return fallbackResults(context);
        }
        List<AiMcpToolDescriptor> selectedTools = selectTools(context, contextMessages, plannerCandidate, tools);
        if (selectedTools.isEmpty()) {
            return fallbackResults(context);
        }
        List<AiDiagnosticToolResult> output = new ArrayList<>();
        for (AiMcpToolDescriptor tool : selectedTools) {
            try {
                AiDiagnosticToolResult result = aiMcpRegistryService.executeTool(tenantId, tool, context);
                if (result != null && result.getSummaryText() != null && !result.getSummaryText().trim().isEmpty()) {
                    output.add(result);
                }
            } catch (Exception e) {
                output.add(new AiDiagnosticToolResult()
                        .setToolCode(tool.getToolCode())
                        .setMountCode(tool.getMountCode())
                        .setMcpToolName(tool.getMcpToolName())
                        .setToolName(tool.getToolName())
                        .setSeverity("WARN")
                        .setSummaryText("MCP工具执行失败:" + e.getMessage()));
            }
        }
        if (output.isEmpty()) {
            return fallbackResults(context);
        }
        return output;
    }
    /**
     * 判断当前请求是否值得启用 MCP。
     * 诊断场景默认启用,普通聊天则按关键词和业务问题特征触发。
     */
    public boolean shouldUseMcp(AiPromptContext context) {
        if (context == null) {
            return false;
        }
        if (AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode())) {
            return true;
        }
        String question = context.getQuestion();
        if (question == null || question.trim().isEmpty()) {
            return false;
        }
        String normalized = question.toLowerCase();
        return normalized.contains("mcp")
                || normalized.contains("工具")
                || normalized.contains("license")
                || question.contains("许可证")
                || normalized.contains("task")
                || normalized.contains("device")
                || normalized.contains("site")
                || normalized.contains("loc")
                || question.contains("库存")
                || question.contains("库位")
                || question.contains("货位")
                || question.contains("物料")
                || question.contains("任务")
                || question.contains("设备")
                || question.contains("站点")
                || question.contains("巷道");
    }
    /**
     * 诊断场景在没有选出具体工具时,退回到内置工具全量收集模式;
     * 普通聊天则直接返回空结果,避免无关工具污染回答。
     */
    private List<AiDiagnosticToolResult> fallbackResults(AiPromptContext context) {
        if (context != null && AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode())) {
            return aiDiagnosticToolService.collect(context);
        }
        return new ArrayList<>();
    }
    /**
     * 按场景和启用状态过滤可用工具目录。
     */
    private List<AiMcpToolDescriptor> filterTools(List<AiMcpToolDescriptor> tools, String sceneCode) {
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        for (AiMcpToolDescriptor tool : tools) {
            if (tool == null || !Integer.valueOf(1).equals(tool.getEnabledFlag())) {
                continue;
            }
            if (sceneCode != null && tool.getSceneCode() != null && !tool.getSceneCode().trim().isEmpty()
                    && !sceneCode.equals(tool.getSceneCode())) {
                continue;
            }
            output.add(tool);
        }
        return output;
    }
    /**
     * 普通聊天优先使用启发式工具选择,诊断场景优先尝试模型规划。
     */
    private List<AiMcpToolDescriptor> selectTools(AiPromptContext context,
                                                  List<AiChatMessage> contextMessages,
                                                  AiModelRouteRuntimeService.RouteCandidate plannerCandidate,
                                                  List<AiMcpToolDescriptor> tools) {
        List<AiMcpToolDescriptor> heuristic = heuristicSelect(tools, context == null ? null : context.getQuestion());
        if (context != null && !AiSceneCode.SYSTEM_DIAGNOSE.equals(context.getSceneCode()) && !heuristic.isEmpty()) {
            return heuristic;
        }
        List<AiMcpToolDescriptor> byModel = selectToolsByModel(context, contextMessages, plannerCandidate, tools);
        if (!byModel.isEmpty()) {
            return byModel;
        }
        return heuristic;
    }
    /**
     * 使用一个轻量规划请求,让模型在现有工具目录中选出最需要调用的工具。
     */
    private List<AiMcpToolDescriptor> selectToolsByModel(AiPromptContext context,
                                                         List<AiChatMessage> contextMessages,
                                                         AiModelRouteRuntimeService.RouteCandidate plannerCandidate,
                                                         List<AiMcpToolDescriptor> tools) {
        if (plannerCandidate == null || plannerCandidate.getRuntimeConfig() == null) {
            return new ArrayList<>();
        }
        try {
            GatewayChatRequest request = new GatewayChatRequest();
            request.setSessionId(context == null ? null : context.getSessionId());
            request.setModelCode(plannerCandidate.getAttemptModelCode());
            request.setRouteCode("system_diagnose_planner");
            request.setAttemptNo(0);
            request.setChatUrl(plannerCandidate.getRuntimeConfig().getChatUrl());
            request.setApiKey(plannerCandidate.getRuntimeConfig().getApiKey());
            request.setModelName(plannerCandidate.getRuntimeConfig().getModelName());
            request.setSystemPrompt(buildPlannerSystemPrompt());
            for (AiChatMessage item : contextMessages) {
                GatewayChatMessage message = new GatewayChatMessage();
                message.setRole(item.getRole());
                message.setContent(item.getContent());
                request.getMessages().add(message);
            }
            GatewayChatMessage plannerMessage = new GatewayChatMessage();
            plannerMessage.setRole("user");
            plannerMessage.setContent(buildPlannerUserPrompt(context, tools));
            request.getMessages().add(plannerMessage);
            String response = aiTextCompletionService.complete(request);
            return mapSelection(parseToolNames(response), tools);
        } catch (Exception ignore) {
            return new ArrayList<>();
        }
    }
    /**
     * 生成专用于工具规划的 system prompt。
     */
    private String buildPlannerSystemPrompt() {
        return "你是WMS诊断工具调度器。你只能从提供的 MCP 工具目录中选择最需要调用的工具。"
                + "只输出 JSON,不要 markdown,不要解释。输出格式必须是 "
                + "{\"tools\":[{\"name\":\"工具名\",\"reason\":\"简短原因\"}]}"
                + "。工具名必须来自目录,最多选择4个;如果无需工具,返回 {\"tools\":[]}。";
    }
    /**
     * 将问题和工具目录整理成规划模型可直接消费的输入。
     */
    private String buildPlannerUserPrompt(AiPromptContext context, List<AiMcpToolDescriptor> tools) {
        List<String> parts = new ArrayList<>();
        parts.add("问题:");
        parts.add(context == null || context.getQuestion() == null ? "" : context.getQuestion());
        parts.add("");
        parts.add("可用工具目录:");
        for (AiMcpToolDescriptor tool : tools) {
            parts.add("- " + tool.getMcpToolName()
                    + " | " + safe(tool.getToolName())
                    + " | " + safe(tool.getDescription())
                    + " | " + safe(tool.getToolPrompt()));
        }
        return String.join("\n", parts);
    }
    /**
     * 解析规划模型返回的 JSON,提取被选中的工具名列表。
     */
    private List<String> parseToolNames(String response) {
        List<String> output = new ArrayList<>();
        if (response == null || response.trim().isEmpty()) {
            return output;
        }
        String normalized = unwrapJson(response);
        try {
            JsonNode root = objectMapper.readTree(normalized);
            JsonNode toolsNode = root.path("tools");
            if (!toolsNode.isArray()) {
                return output;
            }
            for (JsonNode item : toolsNode) {
                String name = item.path("name").asText("");
                if (!name.trim().isEmpty()) {
                    output.add(name.trim());
                }
            }
        } catch (Exception ignore) {
        }
        return output;
    }
    /**
     * 将模型返回的工具名映射回本地工具描述符,并按返回顺序去重截断。
     */
    private List<AiMcpToolDescriptor> mapSelection(List<String> names, List<AiMcpToolDescriptor> tools) {
        Map<String, AiMcpToolDescriptor> byName = new LinkedHashMap<>();
        for (AiMcpToolDescriptor tool : tools) {
            byName.put(tool.getMcpToolName(), tool);
        }
        Set<String> seen = new LinkedHashSet<>();
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        for (String name : names) {
            if (seen.contains(name)) {
                continue;
            }
            AiMcpToolDescriptor tool = byName.get(name);
            if (tool != null) {
                output.add(tool);
                seen.add(name);
            }
            if (output.size() >= 4) {
                break;
            }
        }
        return output;
    }
    /**
     * 基于中文业务关键词与工具描述打分,选出最可能命中的工具。
     */
    private List<AiMcpToolDescriptor> heuristicSelect(List<AiMcpToolDescriptor> tools, String question) {
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        if (tools.isEmpty()) {
            return output;
        }
        String normalized = normalize(question);
        List<ScoredTool> scoredTools = new ArrayList<>();
        for (AiMcpToolDescriptor tool : tools) {
            int score = matchScore(tool, normalized);
            if (score > 0) {
                scoredTools.add(new ScoredTool(tool, score));
            }
        }
        scoredTools.sort(Comparator.comparingInt(ScoredTool::getScore).reversed());
        for (ScoredTool scoredTool : scoredTools) {
            output.add(scoredTool.getTool());
            if (output.size() >= 4) {
                return output;
            }
        }
        for (AiMcpToolDescriptor tool : tools) {
            if (!output.contains(tool)) {
                output.add(tool);
            }
            if (output.size() >= 3) {
                break;
            }
        }
        return output;
    }
    /**
     * 计算单个工具与当前问题的命中分数。
     */
    private int matchScore(AiMcpToolDescriptor tool, String question) {
        if (question == null || question.trim().isEmpty()) {
            return 0;
        }
        return countMatches(question, tool.getToolCode(), tool.getToolName(), tool.getDescription(), tool.getToolPrompt());
    }
    /**
     * 统计问题文本与一组候选字段的片段命中次数。
     */
    private int countMatches(String question, String... values) {
        int score = 0;
        for (String value : values) {
            for (String piece : buildMatchFragments(value)) {
                if (piece.length() >= 2 && question.contains(piece)) {
                    score++;
                }
            }
        }
        return score;
    }
    /**
     * 将工具名、描述等文本拆成适合中文业务查询匹配的片段集合。
     */
    private List<String> buildMatchFragments(String value) {
        List<String> fragments = new ArrayList<>();
        if (value == null || value.trim().isEmpty()) {
            return fragments;
        }
        String[] pieces = normalize(value).split("[^a-z0-9\\u4e00-\\u9fa5]+");
        for (String piece : pieces) {
            if (piece.length() < 2) {
                continue;
            }
            fragments.add(piece);
            if (piece.matches("[\\u4e00-\\u9fa5]+")) {
                if (piece.endsWith("摘要") || piece.endsWith("概况") || piece.endsWith("概览") || piece.endsWith("状态")) {
                    fragments.add(piece.substring(0, piece.length() - 2));
                }
                for (int size = 2; size <= Math.min(4, piece.length()); size++) {
                    for (int i = 0; i <= piece.length() - size; i++) {
                        fragments.add(piece.substring(i, i + size));
                    }
                }
            }
        }
        return fragments;
    }
    /**
     * 对比对文本做小写归一化,便于统一匹配。
     */
    private String normalize(String value) {
        return value == null ? "" : value.toLowerCase();
    }
    /**
     * 尽量从模型输出中剥离出可解析的 JSON 片段。
     */
    private String unwrapJson(String response) {
        String normalized = response.trim();
        if (normalized.startsWith("```")) {
            int firstBrace = normalized.indexOf('{');
            int lastBrace = normalized.lastIndexOf('}');
            if (firstBrace >= 0 && lastBrace > firstBrace) {
                return normalized.substring(firstBrace, lastBrace + 1);
            }
        }
        int firstBrace = normalized.indexOf('{');
        int lastBrace = normalized.lastIndexOf('}');
        if (firstBrace >= 0 && lastBrace > firstBrace) {
            return normalized.substring(firstBrace, lastBrace + 1);
        }
        return normalized;
    }
    /**
     * 为 prompt 组装阶段提供空值安全字符串。
     */
    private String safe(String value) {
        return value == null ? "" : value;
    }
    private static class ScoredTool {
        private final AiMcpToolDescriptor tool;
        private final int score;
        /**
         * 保存工具及其匹配得分。
         */
        private ScoredTool(AiMcpToolDescriptor tool, int score) {
            this.tool = tool;
            this.score = score;
        }
        /**
         * 返回被打分的工具。
         */
        public AiMcpToolDescriptor getTool() {
            return tool;
        }
        /**
         * 返回工具匹配得分。
         */
        public int getScore() {
            return score;
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisPlanRunnerService.java
New file
@@ -0,0 +1,355 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.fasterxml.jackson.databind.JsonNode;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.dto.GatewayChatMessage;
import com.vincent.rsf.server.ai.dto.GatewayChatRequest;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.AiGatewayClient;
import com.vincent.rsf.server.ai.service.AiModelRouteRuntimeService;
import com.vincent.rsf.server.ai.service.AiPromptRuntimeService;
import com.vincent.rsf.server.system.entity.AiDiagnosisPlan;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import com.vincent.rsf.server.system.service.AiDiagnosisPlanService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.InterruptedIOException;
import java.util.Date;
import java.util.List;
@Service
public class AiDiagnosisPlanRunnerService {
    @Resource
    private AiDiagnosisPlanService aiDiagnosisPlanService;
    @Resource
    private AiDiagnosticToolService aiDiagnosticToolService;
    @Resource
    private AiModelRouteRuntimeService aiModelRouteRuntimeService;
    @Resource
    private AiPromptRuntimeService aiPromptRuntimeService;
    @Resource
    private AiGatewayClient aiGatewayClient;
    @Resource
    private AiDiagnosisRuntimeService aiDiagnosisRuntimeService;
    /**
     * 执行一次巡检计划。
     * 计划执行本质上复用诊断主链路,只是输入来源从人工提问变成了计划配置。
     */
    public void runPlan(Long planId, boolean manualTrigger) {
        AiDiagnosisPlan plan = aiDiagnosisPlanService.getById(planId);
        if (plan == null) {
            return;
        }
        Date finishTime = new Date();
        Date nextRunTime = Integer.valueOf(1).equals(plan.getStatus())
                ? aiDiagnosisPlanService.calculateNextRunTime(plan.getCronExpr(), finishTime)
                : null;
        Long userId = plan.getUpdateBy() == null ? plan.getCreateBy() : plan.getUpdateBy();
        String sessionId = "plan-" + plan.getId() + "-" + System.currentTimeMillis();
        String question = plan.getPrompt();
        if (question == null || question.trim().isEmpty()) {
            question = "请对当前WMS系统进行一次巡检诊断,结合库存、任务、设备站点数据识别异常并给出处理建议。";
        }
        AiPromptContext promptContext = new AiPromptContext()
                .setTenantId(plan.getTenantId())
                .setUserId(userId)
                .setSessionId(sessionId)
                .setModelCode(plan.getPreferredModelCode())
                .setQuestion(question)
                .setSceneCode(plan.getSceneCode() == null || plan.getSceneCode().trim().isEmpty()
                        ? AiSceneCode.SYSTEM_DIAGNOSE
                        : plan.getSceneCode());
        List<AiDiagnosticToolResult> diagnosticResults = aiDiagnosticToolService.collect(promptContext);
        String toolSummary = aiDiagnosticToolService.serializeResults(diagnosticResults);
        AiDiagnosisRecord diagnosisRecord = aiDiagnosisRuntimeService.startDiagnosis(
                plan.getTenantId(),
                userId,
                sessionId,
                promptContext.getSceneCode(),
                question
        );
        StringBuilder assistantReply = new StringBuilder();
        String finalModelCode = plan.getPreferredModelCode();
        String finalErrorMessage = null;
        boolean success = false;
        try {
            List<AiModelRouteRuntimeService.RouteCandidate> candidates = aiModelRouteRuntimeService.resolveCandidates(
                    plan.getTenantId(),
                    promptContext.getSceneCode(),
                    plan.getPreferredModelCode()
            );
            if (candidates.isEmpty()) {
                finalErrorMessage = "未找到可用的AI模型配置";
            } else {
                int attemptNo = 1;
                for (AiModelRouteRuntimeService.RouteCandidate candidate : candidates) {
                    AttemptState attemptState = new AttemptState();
                    Date requestTime = new Date();
                    try {
                        GatewayChatRequest gatewayChatRequest = buildGatewayRequest(
                                sessionId,
                                question,
                                candidate,
                                promptContext,
                                diagnosticResults,
                                attemptNo
                        );
                        aiGatewayClient.stream(gatewayChatRequest, event -> handleGatewayEvent(event, assistantReply, attemptState));
                    } catch (Exception e) {
                        attemptState.setSuccess(false);
                        attemptState.setErrorMessage(e.getMessage());
                        attemptState.setInterrupted(isInterruptedError(e));
                        attemptState.setResponseTime(new Date());
                    }
                    if (attemptState.getResponseTime() == null) {
                        attemptState.setResponseTime(new Date());
                    }
                    String actualModelCode = attemptState.getActualModelCode() == null
                            ? candidate.getAttemptModelCode()
                            : attemptState.getActualModelCode();
                    finalModelCode = actualModelCode;
                    aiDiagnosisRuntimeService.saveCallLog(
                            plan.getTenantId(),
                            userId,
                            sessionId,
                            diagnosisRecord.getId(),
                            candidate.getRouteCode(),
                            actualModelCode,
                            attemptNo,
                            requestTime,
                            attemptState.getResponseTime(),
                            Boolean.TRUE.equals(attemptState.getSuccess()) ? 1 : 0,
                            attemptState.getErrorMessage()
                    );
                    if (Boolean.TRUE.equals(attemptState.getSuccess())) {
                        aiModelRouteRuntimeService.markSuccess(candidate.getRouteId());
                        success = assistantReply.length() > 0;
                        if (!success) {
                            finalErrorMessage = "模型未返回有效内容";
                        }
                        break;
                    }
                    if (!attemptState.isInterrupted()) {
                        aiModelRouteRuntimeService.markFailure(candidate.getRouteId());
                    }
                    finalErrorMessage = attemptState.getErrorMessage();
                    if (attemptState.isReceivedDelta() || attemptNo >= candidates.size()) {
                        break;
                    }
                    attemptNo++;
                }
            }
            if (success) {
                aiDiagnosisRuntimeService.finishDiagnosisSuccess(diagnosisRecord, assistantReply.toString(), finalModelCode, toolSummary);
                aiDiagnosisPlanService.finishExecution(
                        plan.getId(),
                        1,
                        diagnosisRecord.getId(),
                        buildPlanMessage(assistantReply.toString(), manualTrigger ? "手动执行成功" : "计划执行成功"),
                        new Date(),
                        nextRunTime
                );
                return;
            }
            aiDiagnosisRuntimeService.finishDiagnosisFailure(diagnosisRecord, assistantReply.toString(), finalErrorMessage, toolSummary);
            aiDiagnosisPlanService.finishExecution(
                    plan.getId(),
                    0,
                    diagnosisRecord.getId(),
                    buildPlanMessage(finalErrorMessage, manualTrigger ? "手动执行失败" : "计划执行失败"),
                    new Date(),
                    nextRunTime
            );
        } catch (Exception e) {
            aiDiagnosisRuntimeService.finishDiagnosisFailure(diagnosisRecord, assistantReply.toString(), e.getMessage(), toolSummary);
            aiDiagnosisPlanService.finishExecution(
                    plan.getId(),
                    0,
                    diagnosisRecord.getId(),
                    buildPlanMessage(e.getMessage(), manualTrigger ? "手动执行失败" : "计划执行失败"),
                    new Date(),
                    nextRunTime
            );
        }
    }
    /**
     * 为巡检计划组装网关请求。
     */
    private GatewayChatRequest buildGatewayRequest(String sessionId,
                                                   String question,
                                                   AiModelRouteRuntimeService.RouteCandidate candidate,
                                                   AiPromptContext promptContext,
                                                   List<AiDiagnosticToolResult> diagnosticResults,
                                                   Integer attemptNo) {
        GatewayChatRequest request = new GatewayChatRequest();
        request.setSessionId(sessionId);
        request.setModelCode(candidate.getAttemptModelCode());
        request.setRouteCode(candidate.getRouteCode());
        request.setAttemptNo(attemptNo);
        request.setSystemPrompt(aiPromptRuntimeService.buildSystemPrompt(
                promptContext.getSceneCode(),
                candidate.getRuntimeConfig().getSystemPrompt(),
                promptContext,
                diagnosticResults
        ));
        request.setChatUrl(candidate.getRuntimeConfig().getChatUrl());
        request.setApiKey(candidate.getRuntimeConfig().getApiKey());
        request.setModelName(candidate.getRuntimeConfig().getModelName());
        GatewayChatMessage message = new GatewayChatMessage();
        message.setRole("user");
        message.setContent(question);
        request.getMessages().add(message);
        return request;
    }
    /**
     * 消费计划执行时的流式网关事件。
     */
    private boolean handleGatewayEvent(JsonNode event, StringBuilder assistantReply, AttemptState attemptState) {
        String type = event.path("type").asText();
        String modelCode = event.path("modelCode").asText();
        if ("delta".equals(type)) {
            String content = event.path("content").asText("");
            assistantReply.append(content);
            attemptState.setReceivedDelta(true);
            attemptState.setActualModelCode(modelCode);
            return true;
        }
        if ("error".equals(type)) {
            attemptState.setSuccess(false);
            attemptState.setActualModelCode(modelCode);
            attemptState.setErrorMessage(event.path("message").asText("模型调用失败"));
            attemptState.setResponseTime(parseResponseTime(event));
            attemptState.setInterrupted(isInterruptedMessage(attemptState.getErrorMessage()));
            return false;
        }
        if ("done".equals(type)) {
            attemptState.setSuccess(true);
            attemptState.setActualModelCode(modelCode);
            attemptState.setResponseTime(parseResponseTime(event));
            return false;
        }
        return true;
    }
    /**
     * 从网关事件中提取响应时间。
     */
    private Date parseResponseTime(JsonNode event) {
        long millis = event.path("responseTime").asLong(0L);
        return millis <= 0L ? new Date() : new Date(millis);
    }
    /**
     * 将计划执行结果压缩成适合回写到计划记录的短消息。
     */
    private String buildPlanMessage(String text, String fallback) {
        String source = text == null ? "" : text.trim();
        if (source.isEmpty()) {
            return fallback;
        }
        return source.length() > 120 ? source.substring(0, 120) : source;
    }
    /**
     * 判断异常链中是否包含中断类错误。
     */
    private boolean isInterruptedError(Throwable throwable) {
        Throwable current = throwable;
        while (current != null) {
            if (current instanceof InterruptedException || current instanceof InterruptedIOException) {
                return true;
            }
            if (isInterruptedMessage(current.getMessage())) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }
    /**
     * 根据异常消息文本判断是否属于连接中断类错误。
     */
    private boolean isInterruptedMessage(String message) {
        if (message == null || message.trim().isEmpty()) {
            return false;
        }
        String normalized = message.toLowerCase();
        return normalized.contains("interrupted")
                || normalized.contains("broken pipe")
                || normalized.contains("connection reset")
                || normalized.contains("forcibly closed");
    }
    private static class AttemptState {
        private Boolean success;
        private String actualModelCode;
        private String errorMessage;
        private boolean receivedDelta;
        private boolean interrupted;
        private Date responseTime;
        public Boolean getSuccess() {
            return success;
        }
        public void setSuccess(Boolean success) {
            this.success = success;
        }
        public String getActualModelCode() {
            return actualModelCode;
        }
        public void setActualModelCode(String actualModelCode) {
            this.actualModelCode = actualModelCode;
        }
        public String getErrorMessage() {
            return errorMessage;
        }
        public void setErrorMessage(String errorMessage) {
            this.errorMessage = errorMessage;
        }
        public boolean isReceivedDelta() {
            return receivedDelta;
        }
        public void setReceivedDelta(boolean receivedDelta) {
            this.receivedDelta = receivedDelta;
        }
        public boolean isInterrupted() {
            return interrupted;
        }
        public void setInterrupted(boolean interrupted) {
            this.interrupted = interrupted;
        }
        public Date getResponseTime() {
            return responseTime;
        }
        public void setResponseTime(Date responseTime) {
            this.responseTime = responseTime;
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisPlanScheduler.java
New file
@@ -0,0 +1,48 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.vincent.rsf.server.system.entity.AiDiagnosisPlan;
import com.vincent.rsf.server.system.service.AiDiagnosisPlanService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@Component
public class AiDiagnosisPlanScheduler {
    @Resource
    private AiDiagnosisPlanService aiDiagnosisPlanService;
    @Resource
    private AiDiagnosisPlanRunnerService aiDiagnosisPlanRunnerService;
    @Resource
    private ThreadPoolTaskScheduler taskScheduler;
    /**
     * 每 30 秒扫描一次到期巡检计划,并异步分发执行。
     */
    @Scheduled(cron = "0/30 * * * * ?")
    public void dispatchDuePlans() {
        Date now = new Date();
        List<AiDiagnosisPlan> plans = aiDiagnosisPlanService.listDuePlans(now);
        for (AiDiagnosisPlan plan : plans) {
            Long operatorId = plan.getUpdateBy() == null ? plan.getCreateBy() : plan.getUpdateBy();
            Date nextRunTime = aiDiagnosisPlanService.calculateNextRunTime(plan.getCronExpr(), now);
            boolean acquired = aiDiagnosisPlanService.acquireForExecution(
                    plan.getId(),
                    operatorId,
                    "计划执行中",
                    nextRunTime
            );
            if (!acquired) {
                continue;
            }
            taskScheduler.execute(() -> aiDiagnosisPlanRunnerService.runPlan(plan.getId(), false));
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisReportService.java
New file
@@ -0,0 +1,182 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiDiagnosisReportService {
    /**
     * 基于模型结论和工具摘要回填诊断报告结构化字段与 Markdown 内容。
     */
    public void fillReport(AiDiagnosisRecord record, String conclusion, String toolSummary) {
        if (record == null) {
            return;
        }
        Map<String, String> sections = extractSections(conclusion);
        String executiveSummary = firstNonBlank(
                sections.get("问题概述"),
                sections.get("执行摘要"),
                safeText(conclusion)
        );
        String evidenceSummary = firstNonBlank(
                sections.get("关键证据"),
                sections.get("证据摘要"),
                safeText(toolSummary)
        );
        String actionSummary = firstNonBlank(
                sections.get("建议动作"),
                sections.get("处置建议"),
                "请结合现场流程继续确认。"
        );
        String riskSummary = firstNonBlank(
                sections.get("风险评估"),
                sections.get("潜在风险"),
                "如未及时处理,可能继续影响当前业务运行。"
        );
        record.setReportTitle(buildTitle(record));
        record.setExecutiveSummary(executiveSummary);
        record.setEvidenceSummary(evidenceSummary);
        record.setActionSummary(actionSummary);
        record.setRiskSummary(riskSummary);
        record.setReportMarkdown(buildMarkdown(record, executiveSummary, evidenceSummary, actionSummary, riskSummary, conclusion));
    }
    /**
     * 尝试从原始结论文本中提取“问题概述/关键证据/建议动作/风险评估”等章节。
     */
    private Map<String, String> extractSections(String conclusion) {
        Map<String, String> sections = new LinkedHashMap<>();
        if (conclusion == null || conclusion.trim().isEmpty()) {
            return sections;
        }
        String[] lines = conclusion.replace("\r", "").split("\n");
        String currentTitle = "问题概述";
        StringBuilder currentBody = new StringBuilder();
        for (String rawLine : lines) {
            String line = rawLine == null ? "" : rawLine.trim();
            String title = normalizeTitle(line);
            if (title != null) {
                saveSection(sections, currentTitle, currentBody);
                currentTitle = title;
                currentBody = new StringBuilder();
                String remainder = line.substring(line.indexOf(title) + title.length()).trim();
                if (remainder.startsWith(":") || remainder.startsWith(":")) {
                    remainder = remainder.substring(1).trim();
                }
                if (!remainder.isEmpty()) {
                    currentBody.append(remainder);
                }
                continue;
            }
            if (currentBody.length() > 0) {
                currentBody.append("\n");
            }
            currentBody.append(rawLine == null ? "" : rawLine);
        }
        saveSection(sections, currentTitle, currentBody);
        return sections;
    }
    /**
     * 将当前章节内容写入 section map。
     */
    private void saveSection(Map<String, String> sections, String title, StringBuilder body) {
        String content = body == null ? "" : body.toString().trim();
        if (content.isEmpty()) {
            return;
        }
        sections.put(title, content);
    }
    /**
     * 判断一行文本是否可视为报告章节标题。
     */
    private String normalizeTitle(String line) {
        if (line == null) {
            return null;
        }
        String normalized = line.replace("#", "").replace(":", "").replace(":", "").trim();
        List<String> titles = new ArrayList<>();
        titles.add("问题概述");
        titles.add("执行摘要");
        titles.add("关键证据");
        titles.add("证据摘要");
        titles.add("可能原因");
        titles.add("建议动作");
        titles.add("处置建议");
        titles.add("风险评估");
        titles.add("潜在风险");
        for (String title : titles) {
            if (normalized.startsWith(title)) {
                return title;
            }
        }
        return null;
    }
    /**
     * 构造诊断报告标题。
     */
    private String buildTitle(AiDiagnosisRecord record) {
        String diagnosisNo = record.getDiagnosisNo() == null ? "-" : record.getDiagnosisNo();
        return "WMS诊断报告-" + diagnosisNo;
    }
    /**
     * 生成最终报告 Markdown。
     */
    private String buildMarkdown(AiDiagnosisRecord record, String executiveSummary, String evidenceSummary,
                                 String actionSummary, String riskSummary, String conclusion) {
        List<String> parts = new ArrayList<>();
        parts.add("# " + buildTitle(record));
        parts.add("");
        parts.add("## 问题概述");
        parts.add(safeText(executiveSummary));
        parts.add("");
        parts.add("## 关键证据");
        parts.add(safeText(evidenceSummary));
        parts.add("");
        parts.add("## 建议动作");
        parts.add(safeText(actionSummary));
        parts.add("");
        parts.add("## 风险评估");
        parts.add(safeText(riskSummary));
        if (conclusion != null && !conclusion.trim().isEmpty()) {
            parts.add("");
            parts.add("## 原始结论");
            parts.add(conclusion.trim());
        }
        return String.join("\n", parts);
    }
    /**
     * 返回第一个非空文本。
     */
    private String firstNonBlank(String... values) {
        if (values == null) {
            return "";
        }
        for (String value : values) {
            if (value != null && !value.trim().isEmpty()) {
                return value.trim();
            }
        }
        return "";
    }
    /**
     * 对展示文本做空值兜底。
     */
    private String safeText(String value) {
        return value == null || value.trim().isEmpty() ? "暂无内容" : value.trim();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosisRuntimeService.java
New file
@@ -0,0 +1,113 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.vincent.rsf.framework.common.SnowflakeIdWorker;
import com.vincent.rsf.server.system.entity.AiCallLog;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import com.vincent.rsf.server.system.service.AiCallLogService;
import com.vincent.rsf.server.system.service.AiDiagnosisRecordService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
@Service
public class AiDiagnosisRuntimeService {
    @Resource
    private AiDiagnosisRecordService aiDiagnosisRecordService;
    @Resource
    private AiCallLogService aiCallLogService;
    @Resource
    private SnowflakeIdWorker snowflakeIdWorker;
    @Resource
    private AiDiagnosisReportService aiDiagnosisReportService;
    /**
     * 在诊断开始时创建一条诊断记录,并标记为“进行中”。
     */
    public AiDiagnosisRecord startDiagnosis(Long tenantId, Long userId, String sessionId, String sceneCode, String question) {
        AiDiagnosisRecord record = new AiDiagnosisRecord()
                .setDiagnosisNo(String.valueOf(snowflakeIdWorker.nextId()).substring(3))
                .setTenantId(tenantId)
                .setUserId(userId)
                .setSessionId(sessionId)
                .setSceneCode(sceneCode)
                .setQuestion(question)
                .setResult(2)
                .setStatus(1)
                .setDeleted(0)
                .setStartTime(new Date())
                .setCreateTime(new Date())
                .setUpdateTime(new Date());
        aiDiagnosisRecordService.save(record);
        return record;
    }
    /**
     * 将诊断记录标记为成功,并补齐报告内容、模型信息和耗时。
     */
    public void finishDiagnosisSuccess(AiDiagnosisRecord record, String conclusion, String modelCode, String toolSummary) {
        if (record == null) {
            return;
        }
        Date now = new Date();
        record.setConclusion(conclusion);
        aiDiagnosisReportService.fillReport(record, conclusion, toolSummary);
        record.setModelCode(modelCode);
        record.setToolSummary(toolSummary);
        record.setResult(1);
        record.setEndTime(now);
        record.setSpendTime(now.getTime() - record.getStartTime().getTime());
        record.setUpdateTime(now);
        aiDiagnosisRecordService.updateById(record);
    }
    /**
     * 将诊断记录标记为失败,并保留已有结论、错误信息和工具轨迹。
     */
    public void finishDiagnosisFailure(AiDiagnosisRecord record, String conclusion, String err, String toolSummary) {
        if (record == null) {
            return;
        }
        Date now = new Date();
        record.setConclusion(conclusion);
        aiDiagnosisReportService.fillReport(record, conclusion, toolSummary);
        record.setToolSummary(toolSummary);
        record.setErr(err);
        record.setResult(0);
        record.setEndTime(now);
        record.setSpendTime(record.getStartTime() == null ? null : now.getTime() - record.getStartTime().getTime());
        record.setUpdateTime(now);
        aiDiagnosisRecordService.updateById(record);
    }
    /**
     * 保存一次模型调用日志,供审计、统计和故障定位使用。
     */
    public AiCallLog saveCallLog(Long tenantId, Long userId, String sessionId, Long diagnosisId, String routeCode,
                                 String modelCode, Integer attemptNo, Date requestTime, Date responseTime,
                                 Integer result, String err) {
        AiCallLog log = new AiCallLog()
                .setTenantId(tenantId)
                .setUserId(userId)
                .setSessionId(sessionId)
                .setDiagnosisId(diagnosisId)
                .setRouteCode(routeCode)
                .setModelCode(modelCode)
                .setAttemptNo(attemptNo)
                .setRequestTime(requestTime)
                .setResponseTime(responseTime)
                .setSpendTime(requestTime == null || responseTime == null ? null : responseTime.getTime() - requestTime.getTime())
                .setResult(result)
                .setErr(err)
                .setStatus(1)
                .setDeleted(0)
                .setCreateTime(responseTime == null ? new Date() : responseTime);
        aiCallLogService.save(log);
        return log;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/diagnosis/AiDiagnosticToolService.java
New file
@@ -0,0 +1,181 @@
package com.vincent.rsf.server.ai.service.diagnosis;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.mcp.AiMcpPayloadMapper;
import com.vincent.rsf.server.ai.service.mcp.AiMcpRegistryService;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiDiagnosticToolService {
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private AiDiagnosticToolConfigService aiDiagnosticToolConfigService;
    @Resource
    private AiMcpRegistryService aiMcpRegistryService;
    @Resource
    private AiMcpPayloadMapper aiMcpPayloadMapper;
    /**
     * 收集当前租户的所有内置诊断工具结果。
     * 这是诊断场景的兜底路径,也用于在模型规划失败时强制聚合一轮内部数据。
     */
    public List<AiDiagnosticToolResult> collect(AiPromptContext context) {
        List<AiDiagnosticToolResult> output = new ArrayList<>();
        for (AiMcpToolDescriptor descriptor : resolveInternalTools(context)) {
            try {
                AiDiagnosticToolResult result = aiMcpRegistryService.executeTool(context.getTenantId(), descriptor, context);
                if (result != null && result.getSummaryText() != null && !result.getSummaryText().trim().isEmpty()) {
                    output.add(result);
                }
            } catch (Exception e) {
                output.add(new AiDiagnosticToolResult()
                        .setToolCode(descriptor.getToolCode())
                        .setMountCode(descriptor.getMountCode() == null ? AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE : descriptor.getMountCode())
                        .setMcpToolName(descriptor.getMcpToolName())
                        .setToolName(descriptor.getToolName())
                        .setSeverity("WARN")
                        .setSummaryText("工具执行失败:" + e.getMessage()));
            }
        }
        return output;
    }
    /**
     * 将工具结果列表转换为可直接拼进系统 Prompt 的摘要文本。
     */
    public String buildPrompt(List<AiDiagnosticToolResult> results) {
        return buildPrompt(null, null, results);
    }
    /**
     * 根据当前场景补充工具级附加规则,并生成最终工具摘要 Prompt。
     */
    public String buildPrompt(Long tenantId, String sceneCode, List<AiDiagnosticToolResult> results) {
        if (results == null || results.isEmpty()) {
            return "";
        }
        Map<String, AiDiagnosticToolConfig> configMap = buildConfigMap(tenantId, sceneCode);
        List<String> parts = new ArrayList<>();
        parts.add("以下是诊断工具返回的实时摘要,请优先依据这些结果判断:");
        for (AiDiagnosticToolResult item : results) {
            AiDiagnosticToolConfig config = configMap.get(item.getToolCode());
            if (config != null && config.getToolPrompt() != null && !config.getToolPrompt().trim().isEmpty()) {
                parts.add("[" + safeValue(item.getToolName()) + "][指令] " + safeValue(config.getToolPrompt()));
            }
            parts.add("[" + safeValue(item.getToolName()) + "][" + safeValue(item.getSeverity()) + "] " + safeValue(item.getSummaryText()));
        }
        return String.join("\n", parts);
    }
    /**
     * 将工具结果序列化成 JSON,便于诊断记录与工具轨迹落库。
     */
    public String serializeResults(List<AiDiagnosticToolResult> results) {
        if (results == null || results.isEmpty()) {
            return "[]";
        }
        try {
            return objectMapper.writeValueAsString(results);
        } catch (Exception e) {
            return "[]";
        }
    }
    private String safeValue(String value) {
        return value == null ? "" : value;
    }
    /**
     * 按当前场景解析可以参与执行的内部工具列表,并按优先级排序。
     */
    private List<AiMcpToolDescriptor> resolveInternalTools(AiPromptContext context) {
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        if (context == null || context.getTenantId() == null) {
            return output;
        }
        for (AiMcpToolDescriptor descriptor : aiMcpRegistryService.listInternalTools(context.getTenantId())) {
            if (descriptor == null || !Integer.valueOf(1).equals(descriptor.getEnabledFlag())) {
                continue;
            }
            if (context.getSceneCode() != null
                    && descriptor.getSceneCode() != null
                    && !descriptor.getSceneCode().trim().isEmpty()
                    && !context.getSceneCode().equals(descriptor.getSceneCode())) {
                continue;
            }
            output.add(descriptor);
        }
        output.sort(Comparator.comparing(
                item -> item.getPriority() == null ? Integer.MAX_VALUE : item.getPriority()
        ));
        return output;
    }
    /**
     * 为当前场景构建工具配置索引。
     * 当工具用途为“聊天与诊断都可用”时,会允许在多个场景下复用同一条配置。
     */
    private Map<String, AiDiagnosticToolConfig> buildConfigMap(Long tenantId, String sceneCode) {
        Map<String, AiDiagnosticToolConfig> map = new LinkedHashMap<>();
        if (tenantId == null || sceneCode == null || sceneCode.trim().isEmpty()) {
            return map;
        }
        List<AiDiagnosticToolConfig> configs = aiDiagnosticToolConfigService.listTenantConfigs(tenantId);
        for (AiDiagnosticToolConfig config : emptyList(configs)) {
            if (!Integer.valueOf(1).equals(config.getStatus())) {
                continue;
            }
            String usageScope = aiMcpPayloadMapper.resolveUsageScope(config.getSceneCode(), config.getEnabledFlag(), config.getUsageScope());
            if (AiMcpConstants.USAGE_SCOPE_DISABLED.equals(usageScope)) {
                continue;
            }
            if (AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY.equals(usageScope)
                    && !sceneCode.equals(config.getSceneCode())) {
                continue;
            }
            AiDiagnosticToolConfig existed = map.get(config.getToolCode());
            if (existed == null || preferConfig(config, existed, sceneCode)) {
                map.put(config.getToolCode(), config);
            }
        }
        return map;
    }
    /**
     * 冲突时优先选择与当前场景精确匹配的配置,其次比较优先级。
     */
    private boolean preferConfig(AiDiagnosticToolConfig candidate, AiDiagnosticToolConfig current, String sceneCode) {
        boolean candidateExact = sceneCode.equals(candidate.getSceneCode());
        boolean currentExact = sceneCode.equals(current.getSceneCode());
        if (candidateExact != currentExact) {
            return candidateExact;
        }
        Integer candidatePriority = candidate.getPriority() == null ? Integer.MAX_VALUE : candidate.getPriority();
        Integer currentPriority = current.getPriority() == null ? Integer.MAX_VALUE : current.getPriority();
        return candidatePriority < currentPriority;
    }
    private <T> Collection<T> emptyList(List<T> list) {
        return list == null ? new ArrayList<>() : list;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiSessionServiceImpl.java
@@ -1,16 +1,24 @@
package com.vincent.rsf.server.ai.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.mapper.AiChatMessageMapper;
import com.vincent.rsf.server.ai.mapper.AiChatSessionMapper;
import com.vincent.rsf.server.ai.model.AiChatMessage;
import com.vincent.rsf.server.ai.model.AiChatSession;
import com.vincent.rsf.server.ai.service.AiRuntimeConfigService;
import com.vincent.rsf.server.ai.service.AiSessionService;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -21,28 +29,68 @@
    private static final ConcurrentMap<String, List<AiChatSession>> LOCAL_SESSION_CACHE = new ConcurrentHashMap<>();
    private static final ConcurrentMap<String, List<AiChatMessage>> LOCAL_MESSAGE_CACHE = new ConcurrentHashMap<>();
    private static final ConcurrentMap<String, String> LOCAL_STOP_CACHE = new ConcurrentHashMap<>();
    private static final String SESSION_TABLE_NAME = "sys_ai_chat_session";
    private static final String MESSAGE_TABLE_NAME = "sys_ai_chat_message";
    @Resource
    private AiRuntimeConfigService aiRuntimeConfigService;
    @Resource
    private AiChatSessionMapper aiChatSessionMapper;
    @Resource
    private AiChatMessageMapper aiChatMessageMapper;
    @Resource
    private DataSource dataSource;
    private volatile boolean storageReady;
    @PostConstruct
    /**
     * 启动时探测聊天存储表是否已创建。
     * 如果表存在则走数据库持久化,否则回退到本地内存缓存,保证开发和缺表场景可继续运行。
     */
    public void initStorageMode() {
        storageReady = detectStorageTables();
    }
    @Override
    /**
     * 读取用户会话列表。
     * 数据库存储模式直接查表,内存模式则从本地缓存取出并按最近更新时间排序。
     */
    public synchronized List<AiChatSession> listSessions(Long tenantId, Long userId) {
        if (useDatabaseStorage()) {
            return aiChatSessionMapper.selectList(new LambdaQueryWrapper<AiChatSession>()
                    .eq(AiChatSession::getTenantId, tenantId)
                    .eq(AiChatSession::getUserId, userId)
                    .orderByDesc(AiChatSession::getUpdateTime, AiChatSession::getCreateTime));
        }
        List<AiChatSession> sessions = getSessions(tenantId, userId);
        sessions.sort(Comparator.comparing(AiChatSession::getUpdateTime, Comparator.nullsLast(Date::compareTo)).reversed());
        return sessions;
    }
    @Override
    /**
     * 创建新会话,并初始化标题、模型和时间戳。
     */
    public synchronized AiChatSession createSession(Long tenantId, Long userId, String title, String modelCode) {
        List<AiChatSession> sessions = getSessions(tenantId, userId);
        List<AiChatSession> sessions = useDatabaseStorage() ? listSessions(tenantId, userId) : getSessions(tenantId, userId);
        Date now = new Date();
        AiChatSession session = new AiChatSession()
                .setId(UUID.randomUUID().toString().replace("-", ""))
                .setTenantId(tenantId)
                .setUserId(userId)
                .setTitle(resolveTitle(title, sessions.size() + 1))
                .setModelCode(resolveModelCode(modelCode))
                .setCreateTime(now)
                .setUpdateTime(now)
                .setLastMessageAt(now);
                .setLastMessageAt(now)
                .setStatus(1)
                .setDeleted(0);
        if (useDatabaseStorage()) {
            aiChatSessionMapper.insert(session);
            return session;
        }
        sessions.add(0, session);
        saveSessions(tenantId, userId, sessions);
        saveMessages(session.getId(), new ArrayList<>());
@@ -50,6 +98,9 @@
    }
    @Override
    /**
     * 确保会话存在;如果会话已存在但模型发生变化,会同步更新会话记录。
     */
    public synchronized AiChatSession ensureSession(Long tenantId, Long userId, String sessionId, String modelCode) {
        AiChatSession session = getSession(tenantId, userId, sessionId);
        if (session == null) {
@@ -64,9 +115,22 @@
    }
    @Override
    /**
     * 安全读取会话,并校验租户与用户归属。
     */
    public synchronized AiChatSession getSession(Long tenantId, Long userId, String sessionId) {
        if (sessionId == null || sessionId.trim().isEmpty()) {
            return null;
        }
        if (useDatabaseStorage()) {
            AiChatSession session = aiChatSessionMapper.selectById(sessionId);
            if (session == null) {
                return null;
            }
            if (!Objects.equals(tenantId, session.getTenantId()) || !Objects.equals(userId, session.getUserId())) {
                return null;
            }
            return session;
        }
        for (AiChatSession session : getSessions(tenantId, userId)) {
            if (sessionId.equals(session.getId())) {
@@ -77,6 +141,9 @@
    }
    @Override
    /**
     * 更新会话标题。
     */
    public synchronized AiChatSession renameSession(Long tenantId, Long userId, String sessionId, String title) {
        AiChatSession session = getSession(tenantId, userId, sessionId);
        if (session == null) {
@@ -89,7 +156,20 @@
    }
    @Override
    /**
     * 删除会话及其关联消息,同时清理停止标记缓存。
     */
    public synchronized void removeSession(Long tenantId, Long userId, String sessionId) {
        if (useDatabaseStorage()) {
            AiChatSession session = getSession(tenantId, userId, sessionId);
            if (session != null) {
                aiChatMessageMapper.delete(new LambdaQueryWrapper<AiChatMessage>()
                        .eq(AiChatMessage::getSessionId, sessionId));
                aiChatSessionMapper.deleteById(sessionId);
            }
            LOCAL_STOP_CACHE.remove(sessionId);
            return;
        }
        List<AiChatSession> sessions = getSessions(tenantId, userId);
        sessions.removeIf(session -> sessionId.equals(session.getId()));
        saveSessions(tenantId, userId, sessions);
@@ -98,15 +178,26 @@
    }
    @Override
    /**
     * 查询会话的完整消息历史。
     */
    public synchronized List<AiChatMessage> listMessages(Long tenantId, Long userId, String sessionId) {
        AiChatSession session = getSession(tenantId, userId, sessionId);
        if (session == null) {
            return new ArrayList<>();
        }
        if (useDatabaseStorage()) {
            return aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>()
                    .eq(AiChatMessage::getSessionId, sessionId)
                    .orderByAsc(AiChatMessage::getCreateTime, AiChatMessage::getId));
        }
        return getMessages(sessionId);
    }
    @Override
    /**
     * 截取最近若干条消息作为模型上下文,避免每次都把完整历史发送给模型。
     */
    public synchronized List<AiChatMessage> listContextMessages(Long tenantId, Long userId, String sessionId, int maxCount) {
        List<AiChatMessage> messages = listMessages(tenantId, userId, sessionId);
        if (messages.size() <= maxCount) {
@@ -116,6 +207,9 @@
    }
    @Override
    /**
     * 追加一条消息,并同步刷新会话摘要、活跃时间和默认标题。
     */
    public synchronized AiChatMessage appendMessage(Long tenantId, Long userId, String sessionId, String role, String content, String modelCode) {
        AiChatSession session = getSession(tenantId, userId, sessionId);
        if (session == null) {
@@ -124,13 +218,21 @@
        List<AiChatMessage> messages = getMessages(sessionId);
        AiChatMessage message = new AiChatMessage()
                .setId(UUID.randomUUID().toString().replace("-", ""))
                .setTenantId(tenantId)
                .setUserId(userId)
                .setSessionId(sessionId)
                .setRole(role)
                .setContent(content)
                .setModelCode(resolveModelCode(modelCode))
                .setCreateTime(new Date());
        messages.add(message);
        saveMessages(sessionId, messages);
                .setCreateTime(new Date())
                .setStatus(1)
                .setDeleted(0);
        if (useDatabaseStorage()) {
            aiChatMessageMapper.insert(message);
        } else {
            messages.add(message);
            saveMessages(sessionId, messages);
        }
        session.setLastMessage(buildPreview(content));
        session.setLastMessageAt(message.getCreateTime());
        session.setUpdateTime(message.getCreateTime());
@@ -145,44 +247,72 @@
    }
    @Override
    /**
     * 清除停止生成标记。
     */
    public void clearStopFlag(String sessionId) {
        LOCAL_STOP_CACHE.remove(sessionId);
    }
    @Override
    /**
     * 标记会话需要停止生成。
     */
    public void requestStop(String sessionId) {
        LOCAL_STOP_CACHE.put(sessionId, "1");
    }
    @Override
    /**
     * 读取停止生成标记。
     */
    public boolean isStopRequested(String sessionId) {
        String stopFlag = LOCAL_STOP_CACHE.get(sessionId);
        return "1".equals(stopFlag);
    }
    /**
     * 从内存缓存中读取当前用户的会话列表。
     */
    private List<AiChatSession> getSessions(Long tenantId, Long userId) {
        String ownerKey = buildOwnerKey(tenantId, userId);
        List<AiChatSession> sessions = LOCAL_SESSION_CACHE.get(ownerKey);
        return sessions == null ? new ArrayList<>() : new ArrayList<>(sessions);
    }
    /**
     * 将会话列表写回本地缓存。
     */
    private void saveSessions(Long tenantId, Long userId, List<AiChatSession> sessions) {
        String ownerKey = buildOwnerKey(tenantId, userId);
        List<AiChatSession> cachedSessions = new ArrayList<>(sessions);
        LOCAL_SESSION_CACHE.put(ownerKey, cachedSessions);
    }
    /**
     * 从内存缓存中读取指定会话的消息列表。
     */
    private List<AiChatMessage> getMessages(String sessionId) {
        List<AiChatMessage> messages = LOCAL_MESSAGE_CACHE.get(sessionId);
        return messages == null ? new ArrayList<>() : new ArrayList<>(messages);
    }
    /**
     * 将消息列表写回本地缓存。
     */
    private void saveMessages(String sessionId, List<AiChatMessage> messages) {
        List<AiChatMessage> cachedMessages = new ArrayList<>(messages);
        LOCAL_MESSAGE_CACHE.put(sessionId, cachedMessages);
    }
    /**
     * 按存储模式刷新单个会话记录。
     */
    private void refreshSession(Long tenantId, Long userId, AiChatSession target) {
        if (useDatabaseStorage()) {
            aiChatSessionMapper.updateById(target);
            return;
        }
        List<AiChatSession> sessions = getSessions(tenantId, userId);
        for (int i = 0; i < sessions.size(); i++) {
            if (target.getId().equals(sessions.get(i).getId())) {
@@ -195,14 +325,23 @@
        saveSessions(tenantId, userId, sessions);
    }
    /**
     * 组装租户与用户维度的本地缓存 key。
     */
    private String buildOwnerKey(Long tenantId, Long userId) {
        return String.valueOf(tenantId) + ":" + String.valueOf(userId);
    }
    /**
     * 解析本次消息使用的模型编码;为空时回退到系统默认模型。
     */
    private String resolveModelCode(String modelCode) {
        return modelCode == null || modelCode.trim().isEmpty() ? aiRuntimeConfigService.resolveDefaultModelCode() : modelCode;
    }
    /**
     * 生成会话标题,未显式传标题时使用“新对话 N”。
     */
    private String resolveTitle(String title, int index) {
        if (title == null || title.trim().isEmpty()) {
            return "新对话 " + index;
@@ -210,6 +349,9 @@
        return buildPreview(title);
    }
    /**
     * 将用户输入压缩成适合作为标题或最后消息预览的短文本。
     */
    private String buildPreview(String content) {
        if (content == null || content.trim().isEmpty()) {
            return "新对话";
@@ -218,4 +360,41 @@
        return normalized.length() > 24 ? normalized.substring(0, 24) : normalized;
    }
    /**
     * 判断当前是否可以使用数据库持久化聊天数据。
     */
    private boolean useDatabaseStorage() {
        return storageReady || (storageReady = detectStorageTables());
    }
    /**
     * 检查聊天存储所需表是否已经存在。
     */
    private boolean detectStorageTables() {
        try (Connection connection = dataSource.getConnection()) {
            return tableExists(connection, SESSION_TABLE_NAME) && tableExists(connection, MESSAGE_TABLE_NAME);
        } catch (Exception ignore) {
            return false;
        }
    }
    /**
     * 判断指定表名是否在当前数据库中存在。
     */
    private boolean tableExists(Connection connection, String tableName) throws Exception {
        if (tableName == null || tableName.trim().isEmpty()) {
            return false;
        }
        String[] candidates = new String[]{tableName, tableName.toUpperCase(), tableName.toLowerCase()};
        for (String candidate : candidates) {
            try (ResultSet resultSet = connection.getMetaData().getTables(connection.getCatalog(), null, candidate, null)) {
                if (resultSet.next()) {
                    return true;
                }
            }
        }
        return false;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpHttpClient.java
New file
@@ -0,0 +1,161 @@
package com.vincent.rsf.server.ai.service.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class AiMcpHttpClient {
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private AiMcpPayloadMapper aiMcpPayloadMapper;
    /**
     * 通过 Streamable HTTP 协议加载远程 MCP 工具目录。
     */
    public List<AiMcpToolDescriptor> listTools(AiMcpMount mount) {
        initialize(mount);
        JsonNode result = sendRequest(mount, "tools/list", new LinkedHashMap<String, Object>(), true);
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        JsonNode toolsNode = result.path("tools");
        if (!toolsNode.isArray()) {
            return output;
        }
        for (JsonNode item : toolsNode) {
            AiMcpToolDescriptor descriptor = aiMcpPayloadMapper.toExternalToolDescriptor(mount, item);
            if (descriptor != null) {
                output.add(descriptor);
            }
        }
        return output;
    }
    /**
     * 通过 Streamable HTTP 协议执行一次远程工具调用。
     */
    public AiDiagnosticToolResult callTool(AiMcpMount mount, String toolName, Map<String, Object> arguments) {
        initialize(mount);
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("name", toolName);
        params.put("arguments", arguments == null ? new LinkedHashMap<String, Object>() : arguments);
        JsonNode result = sendRequest(mount, "tools/call", params, true);
        return aiMcpPayloadMapper.toExternalToolResult(mount, toolName, result);
    }
    /**
     * 执行 MCP initialize + notifications/initialized 握手。
     */
    private void initialize(AiMcpMount mount) {
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("protocolVersion", AiMcpConstants.PROTOCOL_VERSION);
        params.put("capabilities", new LinkedHashMap<String, Object>());
        Map<String, Object> clientInfo = new LinkedHashMap<>();
        clientInfo.put("name", "rsf-server");
        clientInfo.put("version", AiMcpConstants.SERVER_VERSION);
        params.put("clientInfo", clientInfo);
        sendRequest(mount, "initialize", params, true);
        sendRequest(mount, "notifications/initialized", new LinkedHashMap<String, Object>(), false);
    }
    /**
     * 发送一条 JSON-RPC 请求到远程 MCP HTTP 端点。
     */
    private JsonNode sendRequest(AiMcpMount mount, String method, Object params, boolean expectResponse) {
        HttpURLConnection connection = null;
        try {
            connection = (HttpURLConnection) new URL(mount.getUrl()).openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);
            connection.setConnectTimeout(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs());
            connection.setReadTimeout(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs());
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Accept", "application/json");
            applyAuthHeaders(connection, mount);
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("jsonrpc", "2.0");
            if (expectResponse) {
                body.put("id", String.valueOf(System.currentTimeMillis()));
            }
            body.put("method", method);
            body.put("params", params == null ? new LinkedHashMap<String, Object>() : params);
            try (OutputStream outputStream = connection.getOutputStream()) {
                outputStream.write(objectMapper.writeValueAsBytes(body));
                outputStream.flush();
            }
            int statusCode = connection.getResponseCode();
            InputStream inputStream = statusCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
            if (!expectResponse) {
                return null;
            }
            if (inputStream == null) {
                throw new IllegalStateException("MCP服务返回空响应");
            }
            String payload = readPayload(inputStream);
            JsonNode root = objectMapper.readTree(payload);
            if (root.has("error") && !root.get("error").isNull()) {
                throw new IllegalStateException(root.path("error").path("message").asText("MCP调用失败"));
            }
            return root.path("result");
        } catch (Exception e) {
            throw new IllegalStateException("MCP请求失败: " + e.getMessage(), e);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
    /**
     * 按挂载配置写入鉴权请求头。
     */
    private void applyAuthHeaders(HttpURLConnection connection, AiMcpMount mount) {
        if (mount == null || mount.getAuthType() == null || mount.getAuthValue() == null || mount.getAuthValue().trim().isEmpty()) {
            return;
        }
        String authType = mount.getAuthType().trim().toUpperCase();
        if (AiMcpConstants.AUTH_TYPE_BEARER.equals(authType)) {
            connection.setRequestProperty("Authorization", "Bearer " + mount.getAuthValue().trim());
        } else if (AiMcpConstants.AUTH_TYPE_API_KEY.equals(authType)) {
            connection.setRequestProperty("X-API-Key", mount.getAuthValue().trim());
        }
    }
    /**
     * 读取 HTTP 响应体全文。
     */
    private String readPayload(InputStream inputStream) throws Exception {
        StringBuilder output = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line);
            }
        }
        return output.toString();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpPayloadMapper.java
New file
@@ -0,0 +1,204 @@
package com.vincent.rsf.server.ai.service.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class AiMcpPayloadMapper {
    @Resource
    private ObjectMapper objectMapper;
    /**
     * 将远程 MCP tools/list 返回的单个工具定义转换成本系统统一的工具描述符。
     */
    public AiMcpToolDescriptor toExternalToolDescriptor(AiMcpMount mount, JsonNode item) {
        String remoteName = item.path("name").asText("");
        if (remoteName.trim().isEmpty()) {
            return null;
        }
        return new AiMcpToolDescriptor()
                .setMountCode(mount.getMountCode())
                .setMountName(mount.getName())
                .setToolCode(remoteName)
                .setMcpToolName(buildMcpToolName(mount.getMountCode(), remoteName))
                .setToolName(item.path("title").asText(remoteName))
                .setSceneCode(resolveSceneCode(mount.getUsageScope()))
                .setDescription(item.path("description").asText(""))
                .setEnabledFlag(mount.getEnabledFlag())
                .setPriority(999)
                .setToolPrompt(item.path("description").asText(""))
                .setUsageScope(normalizeUsageScope(mount.getUsageScope()))
                .setTransportType(mount.getTransportType())
                .setInputSchema(readInputSchema(item.path("inputSchema"), true));
    }
    /**
     * 将远程 MCP tools/call 返回的结果转换为系统内部统一的工具结果模型。
     */
    public AiDiagnosticToolResult toExternalToolResult(AiMcpMount mount, String toolName, JsonNode result) {
        boolean isError = result.path("isError").asBoolean(false);
        Map<String, Object> rawMeta = new LinkedHashMap<>();
        if (result.has("structuredContent") && !result.get("structuredContent").isNull()) {
            rawMeta.put("structuredContent", objectMapper.convertValue(result.get("structuredContent"), Map.class));
        }
        if (result.has("content") && !result.get("content").isNull()) {
            rawMeta.put("content", objectMapper.convertValue(result.get("content"), Object.class));
        }
        return new AiDiagnosticToolResult()
                .setToolCode(toolName)
                .setMountCode(mount.getMountCode())
                .setMcpToolName(buildMcpToolName(mount.getMountCode(), toolName))
                .setToolName(toolName)
                .setSeverity(isError ? "WARN" : "INFO")
                .setSummaryText(extractContentText(result))
                .setRawMeta(rawMeta);
    }
    /**
     * 将本地工具描述符转换为 MCP 协议 tools/list 所需的数据结构。
     */
    public Map<String, Object> toProtocolTool(AiMcpToolDescriptor descriptor) {
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("name", descriptor.getMcpToolName());
        item.put("title", descriptor.getToolName());
        item.put("description", descriptor.getDescription() == null || descriptor.getDescription().trim().isEmpty()
                ? descriptor.getToolPrompt()
                : descriptor.getDescription());
        item.put("inputSchema", descriptor.getInputSchema() == null || descriptor.getInputSchema().isEmpty()
                ? defaultInputSchema(false)
                : descriptor.getInputSchema());
        return item;
    }
    /**
     * 读取远程工具声明中的 inputSchema;缺省时回退到系统默认输入结构。
     */
    public Map<String, Object> readInputSchema(JsonNode schemaNode, boolean includeTenantId) {
        if (schemaNode == null || schemaNode.isMissingNode() || schemaNode.isNull()) {
            return defaultInputSchema(includeTenantId);
        }
        return objectMapper.convertValue(schemaNode, Map.class);
    }
    /**
     * 生成系统默认的工具输入 schema。
     */
    public Map<String, Object> defaultInputSchema(boolean includeTenantId) {
        Map<String, Object> schema = new LinkedHashMap<>();
        schema.put("type", "object");
        Map<String, Object> properties = new LinkedHashMap<>();
        properties.put("question", fieldSchema("string", "当前诊断问题或调用目的"));
        properties.put("sceneCode", fieldSchema("string", "诊断场景编码"));
        if (includeTenantId) {
            properties.put("tenantId", fieldSchema("integer", "租户ID"));
        }
        schema.put("properties", properties);
        return schema;
    }
    /**
     * 从 MCP tools/call 结果中提取可直接给模型看的文本摘要。
     */
    public String extractContentText(JsonNode result) {
        List<String> parts = new ArrayList<>();
        JsonNode contentNode = result.path("content");
        if (contentNode.isArray()) {
            for (Iterator<JsonNode> it = contentNode.elements(); it.hasNext(); ) {
                JsonNode item = it.next();
                if ("text".equals(item.path("type").asText(""))) {
                    parts.add(item.path("text").asText(""));
                } else if (item.isObject() || item.isArray()) {
                    parts.add(item.toString());
                } else {
                    parts.add(item.asText(""));
                }
            }
        }
        if (!parts.isEmpty()) {
            return String.join("\n", parts).trim();
        }
        if (result.has("structuredContent") && !result.get("structuredContent").isNull()) {
            return result.get("structuredContent").toString();
        }
        return result.toString();
    }
    /**
     * 构造系统统一的 MCP 工具全名。
     */
    public String buildMcpToolName(String mountCode, String toolCode) {
        return (mountCode == null ? AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE : mountCode) + "_" + toolCode;
    }
    /**
     * 将用途预设转换成运行时 sceneCode。
     */
    public String resolveSceneCode(String usageScope) {
        if (AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY.equalsIgnoreCase(usageScope)) {
            return AiSceneCode.SYSTEM_DIAGNOSE;
        }
        return "";
    }
    /**
     * 标准化用途范围字段,统一成系统支持的三个枚举值。
     */
    public String normalizeUsageScope(String usageScope) {
        if (usageScope == null || usageScope.trim().isEmpty()) {
            return AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY;
        }
        String normalized = usageScope.trim().toUpperCase();
        if (AiMcpConstants.USAGE_SCOPE_CHAT_AND_DIAGNOSE.equals(normalized)
                || AiMcpConstants.USAGE_SCOPE_DISABLED.equals(normalized)) {
            return normalized;
        }
        return AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY;
    }
    /**
     * 根据 sceneCode、enabledFlag 和 usageScope 推断最终用途范围。
     */
    public String resolveUsageScope(String sceneCode, Integer enabledFlag, String usageScope) {
        String normalized = normalizeUsageScope(usageScope);
        if (AiMcpConstants.USAGE_SCOPE_DISABLED.equals(normalized)) {
            return normalized;
        }
        if (enabledFlag != null && !Integer.valueOf(1).equals(enabledFlag)) {
            return AiMcpConstants.USAGE_SCOPE_DISABLED;
        }
        if (AiMcpConstants.USAGE_SCOPE_CHAT_AND_DIAGNOSE.equals(normalized)) {
            return normalized;
        }
        if (sceneCode == null || sceneCode.trim().isEmpty()) {
            return AiMcpConstants.USAGE_SCOPE_CHAT_AND_DIAGNOSE;
        }
        return AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY;
    }
    /**
     * 构造单个字段的 schema 定义。
     */
    private Map<String, Object> fieldSchema(String type, String description) {
        Map<String, Object> field = new LinkedHashMap<>();
        field.put("type", type);
        field.put("description", description);
        return field;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpProtocolService.java
New file
@@ -0,0 +1,163 @@
package com.vincent.rsf.server.ai.service.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiMcpProtocolService {
    @Resource
    private AiMcpRegistryService aiMcpRegistryService;
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private AiMcpPayloadMapper aiMcpPayloadMapper;
    /**
     * 处理 MCP 协议入口请求,兼容批量与单条 JSON-RPC 负载。
     */
    public Object handle(Long tenantId, JsonNode body) {
        if (body != null && body.isArray()) {
            List<Object> responses = new ArrayList<>();
            for (JsonNode item : body) {
                Object response = handleSingle(tenantId, item);
                if (response != null) {
                    responses.add(response);
                }
            }
            return responses;
        }
        return handleSingle(tenantId, body);
    }
    /**
     * 处理单条 JSON-RPC MCP 请求。
     */
    public Object handleSingle(Long tenantId, JsonNode request) {
        if (request == null || request.isMissingNode() || request.isNull()) {
            return error(null, -32600, "invalid request");
        }
        JsonNode idNode = request.get("id");
        String method = request.path("method").asText("");
        try {
            if ("initialize".equals(method)) {
                return success(idNode, buildInitializeResult());
            }
            if ("notifications/initialized".equals(method)) {
                return idNode == null || idNode.isNull() ? null : success(idNode, new LinkedHashMap<String, Object>());
            }
            if ("ping".equals(method)) {
                return success(idNode, new LinkedHashMap<String, Object>());
            }
            if ("tools/list".equals(method)) {
                return success(idNode, buildToolsListResult(tenantId));
            }
            if ("tools/call".equals(method)) {
                return success(idNode, buildToolCallResult(tenantId, request.path("params")));
            }
            return error(idNode, -32601, "method not found");
        } catch (Exception e) {
            return error(idNode, -32000, e.getMessage());
        }
    }
    /**
     * 构造 initialize 方法的标准返回体。
     */
    private Map<String, Object> buildInitializeResult() {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("protocolVersion", AiMcpConstants.PROTOCOL_VERSION);
        Map<String, Object> capabilities = new LinkedHashMap<>();
        Map<String, Object> tools = new LinkedHashMap<>();
        tools.put("listChanged", false);
        capabilities.put("tools", tools);
        result.put("capabilities", capabilities);
        Map<String, Object> serverInfo = new LinkedHashMap<>();
        serverInfo.put("name", AiMcpConstants.SERVER_NAME);
        serverInfo.put("version", AiMcpConstants.SERVER_VERSION);
        result.put("serverInfo", serverInfo);
        return result;
    }
    /**
     * 构造 tools/list 方法返回体。
     */
    private Map<String, Object> buildToolsListResult(Long tenantId) {
        List<Map<String, Object>> tools = new ArrayList<>();
        for (AiMcpToolDescriptor descriptor : aiMcpRegistryService.listTools(tenantId, null)) {
            if (descriptor == null || !Integer.valueOf(1).equals(descriptor.getEnabledFlag())) {
                continue;
            }
            tools.add(aiMcpPayloadMapper.toProtocolTool(descriptor));
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("tools", tools);
        return result;
    }
    /**
     * 执行 tools/call,并把内部工具结果转换成 MCP 协议输出。
     */
    private Map<String, Object> buildToolCallResult(Long tenantId, JsonNode paramsNode) {
        String name = paramsNode.path("name").asText("");
        Map<String, Object> arguments = paramsNode.has("arguments") && !paramsNode.get("arguments").isNull()
                ? objectMapper.convertValue(paramsNode.get("arguments"), Map.class)
                : new LinkedHashMap<String, Object>();
        AiPromptContext context = new AiPromptContext()
                .setTenantId(tenantId)
                .setSceneCode(arguments.get("sceneCode") == null ? "system_diagnose" : String.valueOf(arguments.get("sceneCode")))
                .setQuestion(arguments.get("question") == null ? "请执行一次MCP工具调用" : String.valueOf(arguments.get("question")));
        AiDiagnosticToolResult result = aiMcpRegistryService.executeTool(tenantId, name, context, arguments);
        Map<String, Object> payload = new LinkedHashMap<>();
        List<Map<String, Object>> content = new ArrayList<>();
        Map<String, Object> text = new LinkedHashMap<>();
        text.put("type", "text");
        text.put("text", result == null || result.getSummaryText() == null ? "" : result.getSummaryText());
        content.add(text);
        payload.put("content", content);
        payload.put("isError", result != null && "WARN".equalsIgnoreCase(result.getSeverity()));
        if (result != null && result.getRawMeta() != null && !result.getRawMeta().isEmpty()) {
            payload.put("structuredContent", result.getRawMeta());
        }
        return payload;
    }
    /**
     * 生成 JSON-RPC 成功响应。
     */
    public Map<String, Object> success(JsonNode idNode, Object result) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("jsonrpc", "2.0");
        payload.put("id", idNode == null || idNode.isNull() ? null : objectMapper.convertValue(idNode, Object.class));
        payload.put("result", result == null ? new LinkedHashMap<String, Object>() : result);
        return payload;
    }
    /**
     * 生成 JSON-RPC 错误响应。
     */
    public Map<String, Object> error(JsonNode idNode, int code, String message) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("jsonrpc", "2.0");
        payload.put("id", idNode == null || idNode.isNull() ? null : objectMapper.convertValue(idNode, Object.class));
        Map<String, Object> error = new LinkedHashMap<>();
        error.put("code", code);
        error.put("message", message == null ? "unknown error" : message);
        payload.put("error", error);
        return payload;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpRegistryService.java
New file
@@ -0,0 +1,567 @@
package com.vincent.rsf.server.ai.service.mcp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.ai.service.provider.AiDiagnosticDataProvider;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService;
import com.vincent.rsf.server.system.service.AiMcpMountService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class AiMcpRegistryService {
    private static final long EXTERNAL_TOOL_CACHE_TTL_MS = 30000L;
    private final List<AiDiagnosticDataProvider> providers;
    private final Map<String, CachedTools> externalToolCache = new ConcurrentHashMap<>();
    @Resource
    private AiMcpMountService aiMcpMountService;
    @Resource
    private AiDiagnosticToolConfigService aiDiagnosticToolConfigService;
    @Resource
    private AiMcpHttpClient aiMcpHttpClient;
    @Resource
    private AiMcpSseClient aiMcpSseClient;
    @Resource
    private AiMcpPayloadMapper aiMcpPayloadMapper;
    public AiMcpRegistryService(List<AiDiagnosticDataProvider> providers) {
        this.providers = providers == null ? new ArrayList<>() : providers;
    }
    /**
     * 枚举租户下可见的 MCP 工具目录。
     * 包括内部工具和所有启用中的外部挂载工具。
     */
    public List<AiMcpToolDescriptor> listTools(Long tenantId, Long mountId) {
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        List<AiMcpMount> mounts;
        if (mountId == null) {
            mounts = aiMcpMountService.list(new LambdaQueryWrapper<AiMcpMount>()
                    .eq(AiMcpMount::getTenantId, tenantId)
                    .eq(AiMcpMount::getEnabledFlag, 1)
                    .eq(AiMcpMount::getStatus, 1)
                    .orderByAsc(AiMcpMount::getMountCode, AiMcpMount::getId));
        } else {
            mounts = new ArrayList<>();
            AiMcpMount mount = aiMcpMountService.getTenantMount(tenantId, mountId);
            if (mount != null && Integer.valueOf(1).equals(mount.getEnabledFlag()) && Integer.valueOf(1).equals(mount.getStatus())) {
                mounts.add(mount);
            }
        }
        for (AiMcpMount mount : mounts) {
            try {
                if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) {
                    output.addAll(buildInternalTools(tenantId, mount));
                } else if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())) {
                    output.addAll(loadCachedExternalTools(tenantId, mount));
                } else if (AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) {
                    output.addAll(loadCachedExternalTools(tenantId, mount));
                }
            } catch (Exception ignore) {
            }
        }
        return output;
    }
    /**
     * 仅返回系统内置工具目录。
     */
    public List<AiMcpToolDescriptor> listInternalTools(Long tenantId) {
        AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE);
        if (mount == null || !Integer.valueOf(1).equals(mount.getEnabledFlag()) || !Integer.valueOf(1).equals(mount.getStatus())) {
            return new ArrayList<>();
        }
        return buildInternalTools(tenantId, mount);
    }
    /**
     * 测试指定挂载的连通性与工具发现能力,并把结果回写到挂载测试状态字段。
     */
    public Map<String, Object> testMount(Long tenantId, Long mountId) {
        AiMcpMount mount = aiMcpMountService.getTenantMount(tenantId, mountId);
        if (mount == null) {
            throw new IllegalArgumentException("MCP挂载不存在");
        }
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("mountCode", mount.getMountCode());
        payload.put("transportType", mount.getTransportType());
        if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) {
            List<AiMcpToolDescriptor> tools = buildInternalTools(tenantId, mount);
            payload.put("success", true);
            payload.put("toolCount", tools.size());
            payload.put("tools", tools);
            updateTestState(mount, 1, "内部工具挂载正常", tools.size());
            return payload;
        }
        ExternalToolsResult testResult = loadExternalToolsWithTransport(mount);
        List<AiMcpToolDescriptor> tools = testResult.tools;
        payload.put("success", true);
        payload.put("toolCount", tools.size());
        payload.put("tools", tools);
        payload.put("resolvedTransportType", testResult.transportType);
        payload.put("recommendedTransportType", testResult.transportType);
        payload.put("message", buildExternalSuccessMessage(testResult.transportType, tools.size()));
        updateTestState(mount, 1, String.valueOf(payload.get("message")), tools.size());
        return payload;
    }
    /**
     * 以“预览”模式执行一次工具调用,便于后台页面调试工具返回内容。
     */
    public AiDiagnosticToolResult previewTool(Long tenantId, String mountCode, String toolCode, String sceneCode, String question) {
        AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, mountCode);
        if (mount == null) {
            throw new IllegalArgumentException("MCP挂载不存在");
        }
        AiPromptContext context = new AiPromptContext()
                .setTenantId(tenantId)
                .setSceneCode(sceneCode == null || sceneCode.trim().isEmpty() ? AiSceneCode.SYSTEM_DIAGNOSE : sceneCode)
                .setQuestion(question == null || question.trim().isEmpty() ? "请执行一次MCP工具预览" : question);
        if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())
                || AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) {
            return executeExternalTool(mount, toolCode, context, buildToolArguments(context, null));
        }
        return executeInternalTool(mountCode, toolCode, context);
    }
    /**
     * 根据工具描述符执行一次工具调用。
     */
    public AiDiagnosticToolResult executeTool(Long tenantId, AiMcpToolDescriptor descriptor, AiPromptContext context) {
        if (descriptor == null) {
            throw new IllegalArgumentException("MCP工具不存在");
        }
        if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(descriptor.getTransportType())
                || AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(descriptor.getTransportType())) {
            AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, descriptor.getMountCode());
            if (mount == null) {
                throw new IllegalArgumentException("MCP挂载不存在");
            }
            return executeExternalTool(mount, descriptor.getToolCode(), context, buildToolArguments(context, descriptor));
        }
        return executeInternalTool(descriptor.getMountCode(), descriptor.getToolCode(), context);
    }
    /**
     * 根据完整 MCP 工具名执行一次工具调用,通常供协议层直接使用。
     */
    public AiDiagnosticToolResult executeTool(Long tenantId, String mcpToolName, AiPromptContext context, Map<String, Object> arguments) {
        AiMcpToolDescriptor descriptor = findDescriptor(tenantId, mcpToolName);
        if (descriptor == null) {
            throw new IllegalArgumentException("MCP工具不存在");
        }
        if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(descriptor.getTransportType())
                || AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(descriptor.getTransportType())) {
            AiMcpMount mount = aiMcpMountService.getTenantMountByCode(tenantId, descriptor.getMountCode());
            if (mount == null) {
                throw new IllegalArgumentException("MCP挂载不存在");
            }
            return executeExternalTool(mount, descriptor.getToolCode(), context, arguments);
        }
        return executeInternalTool(descriptor.getMountCode(), descriptor.getToolCode(), context);
    }
    /**
     * 确保租户存在默认本地 MCP 挂载。
     */
    public void ensureDefaultMount(Long tenantId, Long userId) {
        if (aiMcpMountService.getTenantMountByCode(tenantId, AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE) != null) {
            return;
        }
        Date now = new Date();
        AiMcpMount mount = new AiMcpMount()
                .setUuid(String.valueOf(System.currentTimeMillis()))
                .setName(AiMcpConstants.DEFAULT_LOCAL_MOUNT_NAME)
                .setMountCode(AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE)
                .setTransportType(AiMcpConstants.TRANSPORT_INTERNAL)
                .setUrl("/ai/mcp")
                .setEnabledFlag(1)
                .setTimeoutMs(10000)
                .setStatus(1)
                .setDeleted(0)
                .setTenantId(tenantId)
                .setCreateBy(userId)
                .setCreateTime(now)
                .setUpdateBy(userId)
                .setUpdateTime(now)
                .setMemo("默认挂载当前 WMS AI 内置工具集合");
        aiMcpMountService.save(mount);
    }
    /**
     * 为内部工具结果补齐挂载编码和标准 MCP 工具名。
     */
    public AiDiagnosticToolResult decorateResult(AiDiagnosticToolResult result) {
        if (result == null) {
            return null;
        }
        return decorateResult(result, AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE, findProvider(result.getToolCode()));
    }
    private AiDiagnosticToolResult decorateResult(AiDiagnosticToolResult result, String mountCode, AiDiagnosticDataProvider provider) {
        String actualMountCode = mountCode == null || mountCode.trim().isEmpty() ? AiMcpConstants.DEFAULT_LOCAL_MOUNT_CODE : mountCode;
        result.setMountCode(actualMountCode);
        result.setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(actualMountCode, result.getToolCode()));
        if ((result.getToolName() == null || result.getToolName().trim().isEmpty()) && provider != null) {
            result.setToolName(provider.getToolName());
        }
        return result;
    }
    /**
     * 按当前租户的工具配置生成内部 MCP 工具目录。
     */
    private List<AiMcpToolDescriptor> buildInternalTools(Long tenantId, AiMcpMount mount) {
        Map<String, AiDiagnosticToolConfig> configMap = buildInternalConfigMap(tenantId);
        List<AiDiagnosticDataProvider> sortedProviders = new ArrayList<>(providers);
        sortedProviders.sort(Comparator.comparingInt(AiDiagnosticDataProvider::getOrder));
        List<AiMcpToolDescriptor> output = new ArrayList<>();
        for (AiDiagnosticDataProvider provider : sortedProviders) {
            AiDiagnosticToolConfig config = configMap.get(provider.getToolCode());
            String usageScope = aiMcpPayloadMapper.resolveUsageScope(
                    config == null ? null : config.getSceneCode(),
                    config == null ? 1 : config.getEnabledFlag(),
                    config == null ? null : config.getUsageScope()
            );
            output.add(new AiMcpToolDescriptor()
                    .setMountCode(mount.getMountCode())
                    .setMountName(mount.getName())
                    .setToolCode(provider.getToolCode())
                    .setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(mount.getMountCode(), provider.getToolCode()))
                    .setToolName(provider.getToolName())
                    .setSceneCode(aiMcpPayloadMapper.resolveSceneCode(usageScope))
                    .setDescription(provider.getDefaultToolPrompt())
                    .setEnabledFlag(AiMcpConstants.USAGE_SCOPE_DISABLED.equals(usageScope) ? 0 : (config == null ? 1 : config.getEnabledFlag()))
                    .setPriority(config == null ? provider.getOrder() : config.getPriority())
                    .setToolPrompt(config == null ? provider.getDefaultToolPrompt() : config.getToolPrompt())
                    .setUsageScope(usageScope)
                    .setTransportType(mount.getTransportType())
                    .setInputSchema(aiMcpPayloadMapper.defaultInputSchema(true)));
        }
        return output;
    }
    /**
     * 为内置工具目录选择一条最合适的有效配置。
     * 目录层只有一条工具描述,因此这里优先保留启用且状态正常的配置,
     * 并优先选择“聊天与诊断都可用”的配置,再退回到“仅诊断”配置。
     */
    private Map<String, AiDiagnosticToolConfig> buildInternalConfigMap(Long tenantId) {
        Map<String, AiDiagnosticToolConfig> configMap = new LinkedHashMap<>();
        for (AiDiagnosticToolConfig item : aiDiagnosticToolConfigService.listTenantConfigs(tenantId)) {
            if (item == null || !Integer.valueOf(1).equals(item.getStatus())) {
                continue;
            }
            AiDiagnosticToolConfig existed = configMap.get(item.getToolCode());
            if (existed == null || preferInternalConfig(item, existed)) {
                configMap.put(item.getToolCode(), item);
            }
        }
        return configMap;
    }
    /**
     * 内置工具目录优先展示用途更广的配置,其次比较优先级。
     */
    private boolean preferInternalConfig(AiDiagnosticToolConfig candidate, AiDiagnosticToolConfig current) {
        String candidateUsageScope = aiMcpPayloadMapper.resolveUsageScope(
                candidate.getSceneCode(),
                candidate.getEnabledFlag(),
                candidate.getUsageScope()
        );
        String currentUsageScope = aiMcpPayloadMapper.resolveUsageScope(
                current.getSceneCode(),
                current.getEnabledFlag(),
                current.getUsageScope()
        );
        int candidateRank = usageScopeRank(candidateUsageScope);
        int currentRank = usageScopeRank(currentUsageScope);
        if (candidateRank != currentRank) {
            return candidateRank < currentRank;
        }
        Integer candidatePriority = candidate.getPriority() == null ? Integer.MAX_VALUE : candidate.getPriority();
        Integer currentPriority = current.getPriority() == null ? Integer.MAX_VALUE : current.getPriority();
        return candidatePriority < currentPriority;
    }
    private int usageScopeRank(String usageScope) {
        if (AiMcpConstants.USAGE_SCOPE_CHAT_AND_DIAGNOSE.equals(usageScope)) {
            return 0;
        }
        if (AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY.equals(usageScope)) {
            return 1;
        }
        return 2;
    }
    /**
     * 按工具编码定位内部工具实现。
     */
    private AiDiagnosticDataProvider findProvider(String toolCode) {
        for (AiDiagnosticDataProvider provider : providers) {
            if (provider.getToolCode().equals(toolCode)) {
                return provider;
            }
        }
        return null;
    }
    /**
     * 更新挂载测试结果和工具数量,并清理缓存。
     */
    private void updateTestState(AiMcpMount mount, Integer result, String message, Integer toolCount) {
        mount.setLastTestResult(result);
        mount.setLastTestMessage(message);
        mount.setLastToolCount(toolCount);
        mount.setLastTestTime(new Date());
        mount.setUpdateTime(new Date());
        aiMcpMountService.updateById(mount);
        if (mount.getId() != null) {
            externalToolCache.remove(buildCacheKey(mount.getTenantId(), mount));
        }
    }
    /**
     * 执行内部 MCP 工具。
     */
    private AiDiagnosticToolResult executeInternalTool(String mountCode, String toolCode, AiPromptContext context) {
        AiDiagnosticDataProvider provider = findProvider(toolCode);
        if (provider == null) {
            throw new IllegalArgumentException("MCP工具不存在");
        }
        AiDiagnosticToolResult result = provider.buildDiagnosticData(context);
        if (result == null) {
            return new AiDiagnosticToolResult()
                    .setToolCode(toolCode)
                    .setMountCode(mountCode)
                    .setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(mountCode, toolCode))
                    .setToolName(provider.getToolName())
                    .setSeverity("WARN")
                    .setSummaryText("工具没有返回结果");
        }
        return decorateResult(result, mountCode, provider);
    }
    /**
     * 执行外部 MCP 工具,并补齐本地系统所需的统一字段。
     */
    private AiDiagnosticToolResult executeExternalTool(AiMcpMount mount, String toolCode,
                                                       AiPromptContext context, Map<String, Object> arguments) {
        ExternalToolCallResult callResult = callExternalTool(mount, toolCode, arguments);
        AiDiagnosticToolResult result = callResult.result;
        return result
                .setToolCode(toolCode)
                .setMountCode(mount.getMountCode())
                .setMcpToolName(aiMcpPayloadMapper.buildMcpToolName(mount.getMountCode(), toolCode))
                .setToolName(result.getToolName() == null || result.getToolName().trim().isEmpty() ? toolCode : result.getToolName())
                .setRawMeta(result.getRawMeta() == null ? new LinkedHashMap<String, Object>() : result.getRawMeta());
    }
    /**
     * 真正加载外部工具目录,不带缓存。
     */
    private List<AiMcpToolDescriptor> loadExternalTools(AiMcpMount mount) {
        return loadExternalToolsWithTransport(mount).tools;
    }
    /**
     * 带短期缓存地加载外部工具目录,避免同一挂载在短时间内重复握手。
     */
    private List<AiMcpToolDescriptor> loadCachedExternalTools(Long tenantId, AiMcpMount mount) {
        String cacheKey = buildCacheKey(tenantId, mount);
        CachedTools cached = externalToolCache.get(cacheKey);
        long now = System.currentTimeMillis();
        if (cached != null && cached.expireAt > now) {
            return new ArrayList<>(cached.tools);
        }
        List<AiMcpToolDescriptor> tools = loadExternalTools(mount);
        externalToolCache.put(cacheKey, new CachedTools(new ArrayList<>(tools), now + EXTERNAL_TOOL_CACHE_TTL_MS));
        return tools;
    }
    /**
     * 生成外部工具目录缓存 key。
     */
    private String buildCacheKey(Long tenantId, AiMcpMount mount) {
        return String.valueOf(tenantId) + ":" + mount.getId() + ":" + (mount.getUpdateTime() == null ? 0L : mount.getUpdateTime().getTime());
    }
    /**
     * 按完整 MCP 工具名反查工具描述符。
     */
    private AiMcpToolDescriptor findDescriptor(Long tenantId, String mcpToolName) {
        for (AiMcpToolDescriptor descriptor : listTools(tenantId, null)) {
            if (descriptor != null && mcpToolName.equals(descriptor.getMcpToolName())) {
                return descriptor;
            }
        }
        return null;
    }
    /**
     * 根据上下文和工具信息构造远程调用参数。
     */
    private Map<String, Object> buildToolArguments(AiPromptContext context, AiMcpToolDescriptor descriptor) {
        Map<String, Object> arguments = new LinkedHashMap<>();
        if (context != null) {
            arguments.put("tenantId", context.getTenantId());
            arguments.put("sceneCode", context.getSceneCode());
            arguments.put("question", context.getQuestion());
            arguments.put("sessionId", context.getSessionId());
        }
        if (descriptor != null) {
            arguments.put("toolName", descriptor.getToolName());
            arguments.put("mountCode", descriptor.getMountCode());
        }
        return arguments;
    }
    /**
     * 生成外部挂载测试成功文案。
     */
    private String buildExternalSuccessMessage(String transportType, int toolCount) {
        String prefix = AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(transportType)
                ? "远程SSE MCP工具加载成功"
                : "远程Streamable HTTP MCP工具加载成功";
        return prefix + ",发现 " + toolCount + " 个工具";
    }
    /**
     * 根据挂载配置选择 HTTP / SSE 客户端去加载外部工具目录。
     * AUTO 模式会依次尝试 Streamable HTTP 和 SSE。
     */
    private ExternalToolsResult loadExternalToolsWithTransport(AiMcpMount mount) {
        if (AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) {
            return new ExternalToolsResult(AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.listTools(mount));
        }
        if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())) {
            return new ExternalToolsResult(AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.listTools(mount));
        }
        List<String> errors = new ArrayList<>();
        try {
            return new ExternalToolsResult(AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.listTools(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_HTTP)));
        } catch (Exception e) {
            errors.add("HTTP: " + e.getMessage());
        }
        try {
            return new ExternalToolsResult(AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.listTools(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_SSE)));
        } catch (Exception e) {
            errors.add("SSE: " + e.getMessage());
        }
        throw new IllegalStateException(String.join(";", errors));
    }
    /**
     * 根据挂载配置选择 HTTP / SSE 客户端执行远程工具。
     */
    private ExternalToolCallResult callExternalTool(AiMcpMount mount, String toolCode, Map<String, Object> arguments) {
        if (AiMcpConstants.TRANSPORT_SSE.equalsIgnoreCase(mount.getTransportType())) {
            return new ExternalToolCallResult(AiMcpConstants.TRANSPORT_SSE, aiMcpSseClient.callTool(mount, toolCode, arguments));
        }
        if (AiMcpConstants.TRANSPORT_HTTP.equalsIgnoreCase(mount.getTransportType())) {
            return new ExternalToolCallResult(AiMcpConstants.TRANSPORT_HTTP, aiMcpHttpClient.callTool(mount, toolCode, arguments));
        }
        List<String> errors = new ArrayList<>();
        try {
            return new ExternalToolCallResult(
                    AiMcpConstants.TRANSPORT_HTTP,
                    aiMcpHttpClient.callTool(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_HTTP), toolCode, arguments)
            );
        } catch (Exception e) {
            errors.add("HTTP: " + e.getMessage());
        }
        try {
            return new ExternalToolCallResult(
                    AiMcpConstants.TRANSPORT_SSE,
                    aiMcpSseClient.callTool(copyMountWithTransport(mount, AiMcpConstants.TRANSPORT_SSE), toolCode, arguments)
            );
        } catch (Exception e) {
            errors.add("SSE: " + e.getMessage());
        }
        throw new IllegalStateException(String.join(";", errors));
    }
    /**
     * 复制一份挂载对象,并覆盖指定传输协议,供 AUTO 探测流程使用。
     */
    private AiMcpMount copyMountWithTransport(AiMcpMount mount, String transportType) {
        return new AiMcpMount()
                .setId(mount.getId())
                .setUuid(mount.getUuid())
                .setName(mount.getName())
                .setMountCode(mount.getMountCode())
                .setTransportType(transportType)
                .setUrl(mount.getUrl())
                .setAuthType(mount.getAuthType())
                .setAuthValue(mount.getAuthValue())
                .setUsageScope(mount.getUsageScope())
                .setEnabledFlag(mount.getEnabledFlag())
                .setTimeoutMs(mount.getTimeoutMs())
                .setStatus(mount.getStatus())
                .setTenantId(mount.getTenantId())
                .setCreateBy(mount.getCreateBy())
                .setCreateTime(mount.getCreateTime())
                .setUpdateBy(mount.getUpdateBy())
                .setUpdateTime(mount.getUpdateTime())
                .setMemo(mount.getMemo());
    }
    private static class CachedTools {
        private final List<AiMcpToolDescriptor> tools;
        private final long expireAt;
        /**
         * 保存一次外部工具目录缓存内容及其失效时间。
         */
        private CachedTools(List<AiMcpToolDescriptor> tools, long expireAt) {
            this.tools = tools;
            this.expireAt = expireAt;
        }
    }
    private static class ExternalToolsResult {
        private final String transportType;
        private final List<AiMcpToolDescriptor> tools;
        /**
         * 保存一次外部工具目录加载结果及实际命中的传输协议。
         */
        private ExternalToolsResult(String transportType, List<AiMcpToolDescriptor> tools) {
            this.transportType = transportType;
            this.tools = tools;
        }
    }
    private static class ExternalToolCallResult {
        private final String transportType;
        private final AiDiagnosticToolResult result;
        /**
         * 保存一次外部工具调用结果及实际命中的传输协议。
         */
        private ExternalToolCallResult(String transportType, AiDiagnosticToolResult result) {
            this.transportType = transportType;
            this.result = result;
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/mcp/AiMcpSseClient.java
New file
@@ -0,0 +1,434 @@
package com.vincent.rsf.server.ai.service.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiMcpToolDescriptor;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@Component
public class AiMcpSseClient {
    private static final Logger logger = LoggerFactory.getLogger(AiMcpSseClient.class);
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private AiMcpPayloadMapper aiMcpPayloadMapper;
    /**
     * 通过 SSE + message endpoint 协议加载远程 MCP 工具目录。
     */
    public List<AiMcpToolDescriptor> listTools(AiMcpMount mount) {
        try (SseSession session = openSession(mount)) {
            logger.info("AI MCP SSE listTools start: mountCode={}, url={}", mount.getMountCode(), mount.getUrl());
            session.initialize();
            JsonNode result = session.request("tools/list", new LinkedHashMap<String, Object>());
            List<AiMcpToolDescriptor> output = new ArrayList<>();
            JsonNode toolsNode = result.path("tools");
            if (!toolsNode.isArray()) {
                logger.warn("AI MCP SSE listTools no tools array: mountCode={}, url={}", mount.getMountCode(), mount.getUrl());
                return output;
            }
            for (JsonNode item : toolsNode) {
                AiMcpToolDescriptor descriptor = aiMcpPayloadMapper.toExternalToolDescriptor(mount, item);
                if (descriptor != null) {
                    output.add(descriptor);
                }
            }
            logger.info("AI MCP SSE listTools success: mountCode={}, url={}, toolCount={}",
                    mount.getMountCode(), mount.getUrl(), output.size());
            return output;
        } catch (Exception e) {
            logger.warn("AI MCP SSE listTools failed: mountCode={}, url={}, message={}",
                    mount.getMountCode(), mount.getUrl(), e.getMessage());
            throw new IllegalStateException("SSE MCP工具加载失败: " + e.getMessage(), e);
        }
    }
    /**
     * 通过 SSE + message endpoint 协议执行远程 MCP 工具。
     */
    public AiDiagnosticToolResult callTool(AiMcpMount mount, String toolName, Map<String, Object> arguments) {
        try (SseSession session = openSession(mount)) {
            logger.info("AI MCP SSE callTool start: mountCode={}, url={}, toolName={}",
                    mount.getMountCode(), mount.getUrl(), toolName);
            session.initialize();
            Map<String, Object> params = new LinkedHashMap<>();
            params.put("name", toolName);
            params.put("arguments", arguments == null ? new LinkedHashMap<String, Object>() : arguments);
            JsonNode result = session.request("tools/call", params);
            AiDiagnosticToolResult toolResult = aiMcpPayloadMapper.toExternalToolResult(mount, toolName, result);
            logger.info("AI MCP SSE callTool success: mountCode={}, url={}, toolName={}, isError={}, summaryLength={}",
                    mount.getMountCode(), mount.getUrl(), toolName,
                    "WARN".equalsIgnoreCase(toolResult.getSeverity()),
                    toolResult.getSummaryText() == null ? 0 : toolResult.getSummaryText().length());
            return toolResult;
        } catch (Exception e) {
            logger.warn("AI MCP SSE callTool failed: mountCode={}, url={}, toolName={}, message={}",
                    mount.getMountCode(), mount.getUrl(), toolName, e.getMessage());
            throw new IllegalStateException("SSE MCP工具调用失败: " + e.getMessage(), e);
        }
    }
    /**
     * 打开远程 SSE 流并创建会话包装对象。
     */
    private SseSession openSession(AiMcpMount mount) throws Exception {
        logger.info("AI MCP SSE opening stream: mountCode={}, url={}", mount.getMountCode(), mount.getUrl());
        int timeoutMs = mount.getTimeoutMs() == null || mount.getTimeoutMs() <= 0 ? 10000 : mount.getTimeoutMs();
        HttpURLConnection connection = (HttpURLConnection) new URL(mount.getUrl()).openConnection();
        connection.setRequestMethod("GET");
        connection.setDoInput(true);
        connection.setConnectTimeout(timeoutMs);
        connection.setReadTimeout(timeoutMs);
        connection.setRequestProperty("Accept", "text/event-stream");
        applyAuthHeaders(connection, mount);
        InputStream inputStream = connection.getInputStream();
        logger.info("AI MCP SSE stream connected: mountCode={}, url={}, responseCode={}",
                mount.getMountCode(), mount.getUrl(), connection.getResponseCode());
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        SseSession session = new SseSession(mount, connection, reader);
        session.start();
        return session;
    }
    private class SseSession implements AutoCloseable {
        private final AiMcpMount mount;
        private final HttpURLConnection connection;
        private final BufferedReader reader;
        private final BlockingQueue<SseEvent> events = new LinkedBlockingQueue<>();
        private volatile boolean closed;
        private Thread worker;
        private String messageEndpoint;
        private SseSession(AiMcpMount mount, HttpURLConnection connection, BufferedReader reader) {
            this.mount = mount;
            this.connection = connection;
            this.reader = reader;
        }
        /**
         * 启动后台读取线程,并等待远程服务返回 endpoint 事件。
         */
        private void start() throws Exception {
            worker = new Thread(this::readLoop, "ai-mcp-sse-client-" + mount.getMountCode());
            worker.setDaemon(true);
            worker.start();
            logger.info("AI MCP SSE waiting endpoint event: mountCode={}, url={}", mount.getMountCode(), mount.getUrl());
            SseEvent endpointEvent = waitEvent("endpoint");
            messageEndpoint = resolveEndpoint(endpointEvent == null ? null : endpointEvent.getData());
            logger.info("AI MCP SSE endpoint event received: mountCode={}, url={}, rawEndpoint={}, resolvedEndpoint={}",
                    mount.getMountCode(), mount.getUrl(),
                    endpointEvent == null ? null : endpointEvent.getData(),
                    messageEndpoint);
            if (messageEndpoint == null || messageEndpoint.trim().isEmpty()) {
                throw new IllegalStateException("SSE MCP未返回 message endpoint");
            }
        }
        /**
         * 完成一次 MCP initialize 握手。
         */
        private void initialize() throws Exception {
            logger.info("AI MCP SSE initialize start: mountCode={}, url={}, messageEndpoint={}",
                    mount.getMountCode(), mount.getUrl(), messageEndpoint);
            Map<String, Object> params = new LinkedHashMap<>();
            params.put("protocolVersion", AiMcpConstants.PROTOCOL_VERSION);
            params.put("capabilities", new LinkedHashMap<String, Object>());
            Map<String, Object> clientInfo = new LinkedHashMap<>();
            clientInfo.put("name", "rsf-server");
            clientInfo.put("version", AiMcpConstants.SERVER_VERSION);
            params.put("clientInfo", clientInfo);
            request("initialize", params);
            notifyInitialized();
            logger.info("AI MCP SSE initialize success: mountCode={}, url={}, messageEndpoint={}",
                    mount.getMountCode(), mount.getUrl(), messageEndpoint);
        }
        /**
         * 向远程 MCP 发送 initialized 通知。
         */
        private void notifyInitialized() throws Exception {
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("jsonrpc", "2.0");
            body.put("method", "notifications/initialized");
            body.put("params", new LinkedHashMap<String, Object>());
            postMessage(body, false);
            logger.info("AI MCP SSE initialized notification sent: mountCode={}, messageEndpoint={}",
                    mount.getMountCode(), messageEndpoint);
        }
        /**
         * 通过 message endpoint 发送一次 JSON-RPC 请求,并等待对应 message 事件响应。
         */
        private JsonNode request(String method, Object params) throws Exception {
            String id = UUID.randomUUID().toString().replace("-", "");
            Map<String, Object> body = new LinkedHashMap<>();
            body.put("jsonrpc", "2.0");
            body.put("id", id);
            body.put("method", method);
            body.put("params", params == null ? new LinkedHashMap<String, Object>() : params);
            logger.info("AI MCP SSE request send: mountCode={}, method={}, requestId={}, messageEndpoint={}",
                    mount.getMountCode(), method, id, messageEndpoint);
            postMessage(body, true);
            SseEvent response = waitEvent("message");
            if (response == null || response.getData() == null || response.getData().trim().isEmpty()) {
                throw new IllegalStateException("SSE MCP未返回响应消息");
            }
            logger.info("AI MCP SSE response received: mountCode={}, method={}, requestId={}, dataLength={}",
                    mount.getMountCode(), method, id, response.getData().length());
            JsonNode root = objectMapper.readTree(response.getData());
            if (!id.equals(root.path("id").asText(""))) {
                logger.warn("AI MCP SSE response id mismatch: mountCode={}, method={}, requestId={}, responseId={}",
                        mount.getMountCode(), method, id, root.path("id").asText(""));
                throw new IllegalStateException("SSE MCP响应ID不匹配");
            }
            if (root.has("error") && !root.get("error").isNull()) {
                logger.warn("AI MCP SSE response error: mountCode={}, method={}, requestId={}, message={}",
                        mount.getMountCode(), method, id, root.path("error").path("message").asText(""));
                throw new IllegalStateException(root.path("error").path("message").asText("MCP调用失败"));
            }
            return root.path("result");
        }
        /**
         * 向 message endpoint 提交一条 HTTP POST 消息。
         */
        private void postMessage(Map<String, Object> body, boolean expectSuccess) throws Exception {
            HttpURLConnection post = null;
            try {
                post = (HttpURLConnection) new URL(messageEndpoint).openConnection();
                post.setRequestMethod("POST");
                post.setDoOutput(true);
                post.setConnectTimeout(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs());
                post.setReadTimeout(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs());
                post.setRequestProperty("Content-Type", "application/json");
                post.setRequestProperty("Accept", "application/json");
                applyAuthHeaders(post, mount);
                logger.info("AI MCP SSE post message: mountCode={}, endpoint={}, method={}",
                        mount.getMountCode(), messageEndpoint, body.get("method"));
                try (OutputStream outputStream = post.getOutputStream()) {
                    outputStream.write(objectMapper.writeValueAsBytes(body));
                    outputStream.flush();
                }
                int statusCode = post.getResponseCode();
                logger.info("AI MCP SSE post response: mountCode={}, endpoint={}, method={}, statusCode={}",
                        mount.getMountCode(), messageEndpoint, body.get("method"), statusCode);
                if (expectSuccess && statusCode >= 400) {
                    throw new IllegalStateException("SSE MCP消息提交失败,状态码=" + statusCode);
                }
            } finally {
                if (post != null) {
                    post.disconnect();
                }
            }
        }
        /**
         * 在限定时间内等待某类 SSE 事件。
         */
        private SseEvent waitEvent(String targetName) throws Exception {
            long timeoutMs = mount.getTimeoutMs() == null ? 10000L : mount.getTimeoutMs().longValue();
            long deadline = System.currentTimeMillis() + timeoutMs;
            while (System.currentTimeMillis() < deadline) {
                long remain = deadline - System.currentTimeMillis();
                SseEvent event = events.poll(remain <= 0 ? 1L : remain, TimeUnit.MILLISECONDS);
                if (event == null) {
                    continue;
                }
                logger.info("AI MCP SSE event dequeued: mountCode={}, target={}, actual={}, dataLength={}",
                        mount.getMountCode(), targetName, event.getName(), event.getData() == null ? 0 : event.getData().length());
                if ("error".equals(event.getName())) {
                    throw new IllegalStateException("SSE MCP事件读取失败: " + event.getData());
                }
                if (targetName.equals(event.getName())) {
                    return event;
                }
            }
            logger.warn("AI MCP SSE wait event timeout: mountCode={}, target={}, timeoutMs={}",
                    mount.getMountCode(), targetName, timeoutMs);
            throw new IllegalStateException("等待SSE事件超时: " + targetName);
        }
        /**
         * 后台持续读取 SSE 流,并把事件转发到队列供主线程消费。
         */
        private void readLoop() {
            String eventName = "message";
            StringBuilder dataBuilder = new StringBuilder();
            try {
                String line;
                while (!closed && (line = reader.readLine()) != null) {
                    if (line.startsWith("event:")) {
                        eventName = line.substring(6).trim();
                        continue;
                    }
                    if (line.startsWith("data:")) {
                        dataBuilder.append(line.substring(5).trim()).append('\n');
                        continue;
                    }
                    if (line.trim().isEmpty()) {
                        if (dataBuilder.length() > 0) {
                            logger.info("AI MCP SSE raw event read: mountCode={}, event={}, dataLength={}",
                                    mount.getMountCode(), eventName, dataBuilder.length());
                            events.offer(new SseEvent(eventName, dataBuilder.toString().trim()));
                        }
                        eventName = "message";
                        dataBuilder.setLength(0);
                    }
                }
            } catch (Exception e) {
                if (!closed) {
                    logger.warn("AI MCP SSE read loop failed: mountCode={}, url={}, message={}",
                            mount.getMountCode(), mount.getUrl(), e.getMessage());
                    events.offer(new SseEvent("error", e.getMessage()));
                }
            } finally {
                try {
                    reader.close();
                } catch (Exception ignore) {
                }
            }
        }
        /**
         * 解析远程返回的 message endpoint。
         * 当对方错误返回回环地址时,会重写成当前挂载 URL 的主机地址。
         */
        private String resolveEndpoint(String rawEndpoint) {
            if (rawEndpoint == null || rawEndpoint.trim().isEmpty()) {
                return null;
            }
            try {
                URI baseUri = new URI(mount.getUrl());
                URI endpointUri = baseUri.resolve(rawEndpoint.trim());
                String host = endpointUri.getHost();
                if (isLoopbackHost(host) && !sameHost(host, baseUri.getHost())) {
                    endpointUri = new URI(
                            baseUri.getScheme(),
                            endpointUri.getUserInfo(),
                            baseUri.getHost(),
                            endpointUri.getPort() > 0 ? endpointUri.getPort() : baseUri.getPort(),
                            endpointUri.getPath(),
                            endpointUri.getQuery(),
                            endpointUri.getFragment());
                    logger.info("AI MCP SSE endpoint rewritten: mountCode={}, rawEndpoint={}, rewrittenEndpoint={}",
                            mount.getMountCode(), rawEndpoint, endpointUri);
                }
                return endpointUri.toString();
            } catch (Exception e) {
                return rawEndpoint.trim();
            }
        }
        /**
         * 判断地址是否为本机回环地址。
         */
        private boolean isLoopbackHost(String host) {
            return "127.0.0.1".equals(host) || "localhost".equalsIgnoreCase(host) || "::1".equals(host);
        }
        /**
         * 判断两个 host 是否等价。
         */
        private boolean sameHost(String left, String right) {
            if (left == null || right == null) {
                return false;
            }
            return left.equalsIgnoreCase(right);
        }
        /**
         * 关闭 SSE 会话,并异步清理底层连接资源。
         */
        @Override
        public void close() throws Exception {
            closed = true;
            logger.info("AI MCP SSE closing session: mountCode={}, url={}", mount.getMountCode(), mount.getUrl());
            if (worker != null) {
                worker.interrupt();
            }
            Thread cleanup = new Thread(() -> {
                try {
                    connection.disconnect();
                } catch (Exception ignore) {
                }
                try {
                    reader.close();
                } catch (Exception ignore) {
                }
                logger.info("AI MCP SSE cleanup finished: mountCode={}, url={}", mount.getMountCode(), mount.getUrl());
            }, "ai-mcp-sse-cleanup-" + mount.getMountCode());
            cleanup.setDaemon(true);
            cleanup.start();
        }
    }
    /**
     * 按挂载配置写入鉴权请求头。
     */
    private void applyAuthHeaders(HttpURLConnection connection, AiMcpMount mount) {
        if (mount == null || mount.getAuthType() == null || mount.getAuthValue() == null || mount.getAuthValue().trim().isEmpty()) {
            return;
        }
        String authType = mount.getAuthType().trim().toUpperCase();
        if (AiMcpConstants.AUTH_TYPE_BEARER.equals(authType)) {
            connection.setRequestProperty("Authorization", "Bearer " + mount.getAuthValue().trim());
        } else if (AiMcpConstants.AUTH_TYPE_API_KEY.equals(authType)) {
            connection.setRequestProperty("X-API-Key", mount.getAuthValue().trim());
        }
    }
    private static class SseEvent {
        private final String name;
        private final String data;
        /**
         * 封装一条 SSE 事件。
         */
        private SseEvent(String name, String data) {
            this.name = name;
            this.data = data;
        }
        /**
         * 返回事件名。
         */
        public String getName() {
            return name;
        }
        /**
         * 返回事件数据文本。
         */
        public String getData() {
            return data;
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiApiFailureSummaryService.java
New file
@@ -0,0 +1,101 @@
package com.vincent.rsf.server.ai.service.provider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.system.entity.AiCallLog;
import com.vincent.rsf.server.system.service.AiCallLogService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiApiFailureSummaryService implements AiDiagnosticDataProvider {
    private static final String TOOL_CODE = "ai_call_failure";
    private static final String TOOL_NAME = "AI调用失败";
    @Resource
    private AiCallLogService aiCallLogService;
    @Resource
    private com.vincent.rsf.server.ai.config.AiProperties aiProperties;
    /**
     * 返回 AI 调用失败工具默认顺序。
     */
    @Override
    public int getOrder() {
        return 50;
    }
    /**
     * 返回 AI 调用失败工具编码。
     */
    @Override
    public String getToolCode() {
        return TOOL_CODE;
    }
    /**
     * 返回 AI 调用失败工具展示名。
     */
    @Override
    public String getToolName() {
        return TOOL_NAME;
    }
    /**
     * 返回 AI 调用失败工具默认说明。
     */
    @Override
    public String getDefaultToolPrompt() {
        return "重点识别最近 AI 调用失败的模型、错误类型和时间窗口。";
    }
    /**
     * 汇总最近一段时间内的 AI 调用失败记录。
     */
    @Override
    public AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context) {
        Date start = new Date(System.currentTimeMillis() - aiProperties.getApiFailureWindowHours() * 3600_000L);
        List<AiCallLog> records = aiCallLogService.list(new LambdaQueryWrapper<AiCallLog>()
                .eq(AiCallLog::getTenantId, context.getTenantId())
                .eq(AiCallLog::getResult, 0)
                .ge(AiCallLog::getCreateTime, start)
                .orderByDesc(AiCallLog::getCreateTime)
                .last("limit 10"));
        Map<String, Object> meta = new LinkedHashMap<>();
        meta.put("count", records.size());
        if (records.isEmpty()) {
            return new AiDiagnosticToolResult()
                    .setToolCode(getToolCode())
                    .setToolName(getToolName())
                    .setSeverity("INFO")
                    .setSummaryText("最近 " + aiProperties.getApiFailureWindowHours() + " 小时未发现 AI 调用失败记录。")
                    .setRawMeta(meta);
        }
        List<String> parts = new ArrayList<>();
        for (AiCallLog item : records) {
            parts.add((item.getModelCode() == null ? "未知模型" : item.getModelCode())
                    + "("
                    + (item.getErr() == null ? "无异常描述" : item.getErr())
                    + ")");
        }
        meta.put("latestErrors", parts);
        return new AiDiagnosticToolResult()
                .setToolCode(getToolCode())
                .setToolName(getToolName())
                .setSeverity("WARN")
                .setSummaryText("最近 " + aiProperties.getApiFailureWindowHours() + " 小时发现 " + records.size() + " 条 AI 调用失败记录,最近失败包括:" + String.join(";", parts))
                .setRawMeta(meta);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiDeviceSiteSummaryService.java
New file
@@ -0,0 +1,159 @@
package com.vincent.rsf.server.ai.service.provider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.manager.entity.DeviceSite;
import com.vincent.rsf.server.manager.mapper.DeviceSiteMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiDeviceSiteSummaryService implements AiDiagnosticDataProvider {
    private static final String TOOL_CODE = "device_site_summary";
    private static final String TOOL_NAME = "设备站点摘要";
    @Resource
    private DeviceSiteMapper deviceSiteMapper;
    /**
     * 返回设备站点工具默认顺序。
     */
    @Override
    public int getOrder() {
        return 30;
    }
    /**
     * 返回设备站点工具编码。
     */
    @Override
    public String getToolCode() {
        return TOOL_CODE;
    }
    /**
     * 返回设备站点工具展示名。
     */
    @Override
    public String getToolName() {
        return TOOL_NAME;
    }
    /**
     * 返回设备站点工具默认说明。
     */
    @Override
    public String getDefaultToolPrompt() {
        return "结合设备站点摘要判断设备状态、巷道分布和最近更新站点。";
    }
    /**
     * 汇总站点状态、设备分布和最近更新站点,生成设备站点摘要。
     */
    @Override
    public AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context) {
        String summary = buildDeviceSiteSummary(context);
        String severity = summary.contains("未查询到") ? "WARN" : "INFO";
        return new AiDiagnosticToolResult()
                .setToolCode(getToolCode())
                .setToolName(getToolName())
                .setSeverity(severity)
                .setSummaryText(summary);
    }
    /**
     * 基于 man_device_site 生成设备站点总览。
     */
    private String buildDeviceSiteSummary(AiPromptContext context) {
        List<DeviceSite> deviceSites = deviceSiteMapper.selectList(new LambdaQueryWrapper<DeviceSite>()
                .select(DeviceSite::getSite, DeviceSite::getName, DeviceSite::getType, DeviceSite::getDevice,
                        DeviceSite::getDeviceCode, DeviceSite::getDeviceSite, DeviceSite::getStatus,
                        DeviceSite::getChannel, DeviceSite::getUpdateTime)
                .eq(DeviceSite::getTenantId, context.getTenantId())
                .orderByDesc(DeviceSite::getUpdateTime)
                .last("limit 50"));
        if (deviceSites.isEmpty()) {
            return "当前未查询到 man_device_site 设备站点数据,请提示用户核对基础数据或租户配置。";
        }
        Map<String, Long> statusCounters = new LinkedHashMap<>();
        Map<String, Long> deviceCounters = new LinkedHashMap<>();
        Map<String, Long> channelCounters = new LinkedHashMap<>();
        for (DeviceSite deviceSite : deviceSites) {
            String status = resolveStatus(deviceSite.getStatus());
            String device = emptyToDefault(deviceSite.getDevice$(), "未配置设备类型");
            String channel = deviceSite.getChannel() == null ? "未配置巷道" : "巷道" + deviceSite.getChannel();
            statusCounters.put(status, statusCounters.getOrDefault(status, 0L) + 1);
            deviceCounters.put(device, deviceCounters.getOrDefault(device, 0L) + 1);
            channelCounters.put(channel, channelCounters.getOrDefault(channel, 0L) + 1);
        }
        StringBuilder summary = new StringBuilder();
        summary.append("以下是基于 man_device_site 的实时设备站点摘要,请结合任务和库存数据综合判断:");
        summary.append("\n设备站点总览:共 ").append(deviceSites.size()).append(" 条有效站点记录。");
        summary.append("\n站点状态分布:").append(formatCounter(statusCounters)).append("。");
        summary.append("\n设备类型分布:").append(formatCounter(deviceCounters)).append("。");
        summary.append("\n巷道分布:").append(formatCounter(channelCounters)).append("。");
        summary.append("\n最近更新站点 TOP5:").append(formatLatestSites(deviceSites.subList(0, Math.min(deviceSites.size(), 5)))).append("。");
        return summary.toString();
    }
    /**
     * 格式化计数类摘要。
     */
    private String formatCounter(Map<String, Long> counter) {
        List<String> parts = new ArrayList<>();
        for (Map.Entry<String, Long> item : counter.entrySet()) {
            parts.add(item.getKey() + " " + item.getValue() + " 条");
        }
        return String.join(",", parts);
    }
    /**
     * 格式化最近更新站点列表。
     */
    private String formatLatestSites(List<DeviceSite> deviceSites) {
        List<String> parts = new ArrayList<>();
        for (DeviceSite deviceSite : deviceSites) {
            parts.add(emptyToDefault(deviceSite.getName(), emptyToDefault(deviceSite.getSite(), "未命名站点"))
                    + "("
                    + resolveStatus(deviceSite.getStatus())
                    + ",设备 "
                    + emptyToDefault(deviceSite.getDevice$(), "-")
                    + ",接驳位 "
                    + emptyToDefault(deviceSite.getDeviceCode(), "-")
                    + ",巷道 "
                    + (deviceSite.getChannel() == null ? "-" : deviceSite.getChannel())
                    + ")");
        }
        return String.join(";", parts);
    }
    /**
     * 将站点状态码转成可读文本。
     */
    private String resolveStatus(Integer status) {
        if (status == null) {
            return "未知状态";
        }
        return status == 1 ? "正常" : status == 0 ? "冻结" : "状态" + status;
    }
    /**
     * 统一处理空值展示。
     */
    private String emptyToDefault(String value, String defaultValue) {
        return value == null || value.trim().isEmpty() ? defaultValue : value;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiDiagnosticDataProvider.java
New file
@@ -0,0 +1,38 @@
package com.vincent.rsf.server.ai.service.provider;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
public interface AiDiagnosticDataProvider {
    /**
     * 返回内部工具编码,作为本地 MCP 工具名后缀和工具配置主键。
     */
    String getToolCode();
    /**
     * 返回工具展示名称。
     */
    String getToolName();
    /**
     * 返回工具默认说明,用于工具目录展示和默认 Prompt 引导。
     */
    default String getDefaultToolPrompt() {
        return "";
    }
    /**
     * 返回工具默认顺序。
     */
    int getOrder();
    /**
     * 执行内部工具的真实业务查询,并返回摘要化结果。
     */
    AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context);
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiOperationRecordSummaryService.java
New file
@@ -0,0 +1,101 @@
package com.vincent.rsf.server.ai.service.provider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.system.entity.OperationRecord;
import com.vincent.rsf.server.system.service.OperationRecordService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Service
public class AiOperationRecordSummaryService implements AiDiagnosticDataProvider {
    private static final String TOOL_CODE = "operation_record";
    private static final String TOOL_NAME = "异常操作日志";
    @Resource
    private OperationRecordService operationRecordService;
    @Resource
    private com.vincent.rsf.server.ai.config.AiProperties aiProperties;
    /**
     * 返回异常操作日志工具默认顺序。
     */
    @Override
    public int getOrder() {
        return 40;
    }
    /**
     * 返回异常操作日志工具编码。
     */
    @Override
    public String getToolCode() {
        return TOOL_CODE;
    }
    /**
     * 返回异常操作日志工具展示名。
     */
    @Override
    public String getToolName() {
        return TOOL_NAME;
    }
    /**
     * 返回异常操作日志工具默认说明。
     */
    @Override
    public String getDefaultToolPrompt() {
        return "重点识别最近失败操作和高频异常。";
    }
    /**
     * 汇总最近一段时间内的失败操作记录。
     */
    @Override
    public AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context) {
        Date start = new Date(System.currentTimeMillis() - aiProperties.getDiagnosticLogWindowHours() * 3600_000L);
        List<OperationRecord> records = operationRecordService.list(new LambdaQueryWrapper<OperationRecord>()
                .eq(OperationRecord::getTenantId, context.getTenantId())
                .eq(OperationRecord::getResult, 0)
                .ge(OperationRecord::getCreateTime, start)
                .orderByDesc(OperationRecord::getCreateTime)
                .last("limit 10"));
        Map<String, Object> meta = new LinkedHashMap<>();
        meta.put("count", records.size());
        if (records.isEmpty()) {
            return new AiDiagnosticToolResult()
                    .setToolCode(getToolCode())
                    .setToolName(getToolName())
                    .setSeverity("INFO")
                    .setSummaryText("最近 " + aiProperties.getDiagnosticLogWindowHours() + " 小时未发现操作日志失败记录。")
                    .setRawMeta(meta);
        }
        List<String> parts = new ArrayList<>();
        for (OperationRecord item : records) {
            parts.add((item.getNamespace() == null ? "未知操作" : item.getNamespace())
                    + "("
                    + (item.getErr() == null ? "无异常描述" : item.getErr())
                    + ")");
        }
        meta.put("latestErrors", parts);
        return new AiDiagnosticToolResult()
                .setToolCode(getToolCode())
                .setToolName(getToolName())
                .setSeverity("WARN")
                .setSummaryText("最近 " + aiProperties.getDiagnosticLogWindowHours() + " 小时发现 " + records.size() + " 条失败操作记录,最近异常包括:" + String.join(";", parts))
                .setRawMeta(meta);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiTaskSummaryService.java
File was renamed from rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiTaskSummaryService.java
@@ -1,6 +1,7 @@
package com.vincent.rsf.server.ai.service;
package com.vincent.rsf.server.ai.service.provider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.manager.entity.Task;
import com.vincent.rsf.server.manager.enums.TaskStsType;
@@ -12,27 +13,62 @@
import java.util.*;
@Service
public class AiTaskSummaryService implements AiPromptContextProvider {
public class AiTaskSummaryService implements AiDiagnosticDataProvider {
    private static final String TOOL_CODE = "task_summary";
    private static final String TOOL_NAME = "任务摘要";
    @Resource
    private TaskMapper taskMapper;
    /**
     * 返回任务工具默认顺序。
     */
    @Override
    public boolean supports(AiPromptContext context) {
        if (context == null || context.getQuestion() == null) {
            return false;
        }
        String normalized = context.getQuestion().toLowerCase(Locale.ROOT);
        return normalized.contains("task")
                || normalized.contains("任务")
                || normalized.contains("出库任务")
                || normalized.contains("入库任务")
                || normalized.contains("移库任务")
                || normalized.contains("备货任务");
    public int getOrder() {
        return 20;
    }
    /**
     * 返回任务工具编码。
     */
    @Override
    public String buildContext(AiPromptContext context) {
    public String getToolCode() {
        return TOOL_CODE;
    }
    /**
     * 返回任务工具展示名。
     */
    @Override
    public String getToolName() {
        return TOOL_NAME;
    }
    /**
     * 返回任务工具默认说明。
     */
    @Override
    public String getDefaultToolPrompt() {
        return "结合任务摘要识别积压、异常状态和最近变更任务。";
    }
    /**
     * 汇总任务状态和最近变更任务,生成任务摘要工具结果。
     */
    @Override
    public AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context) {
        return new AiDiagnosticToolResult()
                .setToolCode(getToolCode())
                .setToolName(getToolName())
                .setSeverity("INFO")
                .setSummaryText(buildTaskSummary());
    }
    /**
     * 基于 man_task 生成任务总览、状态分布、类型分布和最近任务摘要。
     */
    private String buildTaskSummary() {
        List<Task> activeTasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
                .select(Task::getTaskCode, Task::getTaskStatus, Task::getTaskType, Task::getOrgLoc, Task::getTargLoc, Task::getUpdateTime)
                .eq(Task::getStatus, 1));
@@ -75,6 +111,9 @@
        return summary.toString();
    }
    /**
     * 格式化任务状态统计。
     */
    private String formatStatuses(Map<Integer, Long> rows) {
        List<String> parts = new ArrayList<>();
        for (Map.Entry<Integer, Long> row : rows.entrySet()) {
@@ -83,6 +122,9 @@
        return String.join(",", parts);
    }
    /**
     * 格式化任务类型统计。
     */
    private String formatTypes(Map<Integer, Long> rows) {
        List<String> parts = new ArrayList<>();
        for (Map.Entry<Integer, Long> row : rows.entrySet()) {
@@ -91,6 +133,9 @@
        return String.join(",", parts);
    }
    /**
     * 格式化最近更新任务列表。
     */
    private String formatLatestTasks(List<Task> tasks) {
        List<String> parts = new ArrayList<>();
        for (Task task : tasks) {
@@ -106,6 +151,9 @@
        return String.join(";", parts);
    }
    /**
     * 将任务状态编码转换为可读文案。
     */
    private String resolveTaskStatus(Integer taskStatus) {
        if (taskStatus == null) {
            return "未知状态";
@@ -118,6 +166,9 @@
        return "状态" + taskStatus;
    }
    /**
     * 将任务类型编码转换为可读文案。
     */
    private String resolveTaskType(Integer taskType) {
        if (taskType == null) {
            return "未知类型";
@@ -130,7 +181,13 @@
        return "类型" + taskType;
    }
    /**
     * 统一处理空字符串显示。
     */
    private String emptyToDash(String value) {
        return value == null || value.trim().isEmpty() ? "-" : value;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/ai/service/provider/AiWarehouseSummaryService.java
File was renamed from rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiWarehouseSummaryService.java
@@ -1,6 +1,7 @@
package com.vincent.rsf.server.ai.service;
package com.vincent.rsf.server.ai.service.provider;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.ai.model.AiDiagnosticToolResult;
import com.vincent.rsf.server.ai.model.AiPromptContext;
import com.vincent.rsf.server.manager.entity.Loc;
import com.vincent.rsf.server.manager.entity.LocItem;
@@ -13,7 +14,10 @@
import java.util.*;
@Service
public class AiWarehouseSummaryService implements AiPromptContextProvider {
public class AiWarehouseSummaryService implements AiDiagnosticDataProvider {
    private static final String TOOL_CODE = "warehouse_summary";
    private static final String TOOL_NAME = "库存摘要";
    private static final Map<String, String> LOC_STATUS_LABELS = new LinkedHashMap<>();
@@ -31,16 +35,54 @@
    @Resource
    private LocItemMapper locItemMapper;
    /**
     * 返回库存类内部工具的默认顺序。
     */
    @Override
    public boolean supports(AiPromptContext context) {
        return context != null && shouldSummarize(context.getQuestion());
    public int getOrder() {
        return 10;
    }
    /**
     * 返回库存工具编码。
     */
    @Override
    public String buildContext(AiPromptContext context) {
        if (!supports(context)) {
            return "";
        }
    public String getToolCode() {
        return TOOL_CODE;
    }
    /**
     * 返回库存工具展示名。
     */
    @Override
    public String getToolName() {
        return TOOL_NAME;
    }
    /**
     * 返回库存工具默认说明。
     */
    @Override
    public String getDefaultToolPrompt() {
        return "结合库存摘要判断库位状态、库存结构与重点物料分布。";
    }
    /**
     * 汇总库位与库存明细,生成库存摘要工具结果。
     */
    @Override
    public AiDiagnosticToolResult buildDiagnosticData(AiPromptContext context) {
        return new AiDiagnosticToolResult()
                .setToolCode(getToolCode())
                .setToolName(getToolName())
                .setSeverity("INFO")
                .setSummaryText(buildWarehouseSummary(context));
    }
    /**
     * 基于 man_loc 和 man_loc_item 生成库存概览、库位状态分布和 TOP 统计。
     */
    private String buildWarehouseSummary(AiPromptContext context) {
        List<Loc> activeLocs = locMapper.selectList(new LambdaQueryWrapper<Loc>()
                .select(Loc::getUseStatus)
@@ -134,21 +176,9 @@
        return summary.toString();
    }
    private boolean shouldSummarize(String question) {
        if (question == null || question.trim().isEmpty()) {
            return false;
        }
        String normalized = question.toLowerCase(Locale.ROOT);
        return normalized.contains("loc")
                || normalized.contains("库位")
                || normalized.contains("货位")
                || normalized.contains("库区")
                || normalized.contains("库存")
                || normalized.contains("物料")
                || normalized.contains("巷道")
                || normalized.contains("储位");
    }
    /**
     * 将库位状态计数格式化为可读文本。
     */
    private String formatLocStatuses(Map<String, Long> counters) {
        if (counters == null || counters.isEmpty()) {
            return "暂无数据";
@@ -160,6 +190,9 @@
        return String.join(",", parts);
    }
    /**
     * 格式化库存最多的库位列表。
     */
    private String formatTopLocs(List<Map.Entry<String, LocAggregate>> rows) {
        List<String> parts = new ArrayList<>();
        for (Map.Entry<String, LocAggregate> row : rows) {
@@ -170,6 +203,9 @@
        return String.join(";", parts);
    }
    /**
     * 格式化库存最多的物料列表。
     */
    private String formatTopMaterials(List<MaterialAggregate> rows) {
        List<String> parts = new ArrayList<>();
        for (MaterialAggregate row : rows) {
@@ -183,11 +219,17 @@
        return String.join(";", parts);
    }
    /**
     * 统一格式化数量值。
     */
    private String formatDecimal(Object value) {
        BigDecimal decimal = toDecimal(value);
        return decimal.stripTrailingZeros().toPlainString();
    }
    /**
     * 将不同类型的数量字段统一转换为 BigDecimal。
     */
    private BigDecimal toDecimal(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
@@ -213,3 +255,6 @@
        private final Set<String> locCodes = new HashSet<>();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RemotesInfoProperties.java
@@ -73,3 +73,4 @@
        return  this.host + ":" + this.port + "/" + this.prePath;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/config/RestTemplateConfig.java
@@ -20,3 +20,4 @@
        return  new RestTemplate();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/WcsController.java
@@ -191,3 +191,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/BaseInfoController.java
@@ -128,3 +128,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/ErpQueryController.java
@@ -140,3 +140,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/ReportMsgController.java
@@ -59,3 +59,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/SyncOrderController.java
@@ -218,3 +218,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/BaseMatParms.java
@@ -53,3 +53,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/BaseSyncParams.java
@@ -10,3 +10,4 @@
public class BaseSyncParams {
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/CheckObjParams.java
@@ -21,3 +21,4 @@
    @ApiModelProperty("物料编码")
    private String matnrCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/CompaniesParam.java
@@ -51,3 +51,4 @@
    private String code;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/InventoryQueryConditionParam.java
@@ -139,3 +139,4 @@
        return map;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/LocAreasParams.java
@@ -25,3 +25,4 @@
    private String type;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ManualShelvingParams.java
@@ -24,3 +24,4 @@
    @ApiModelProperty("单据明细")
    private List<WaitPakinItem> itemList;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OpStockParams.java
@@ -17,3 +17,4 @@
    @ApiModelProperty("物料编码")
    private String matnrCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OrderItem.java
@@ -51,3 +51,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OrderParams.java
@@ -59,3 +59,4 @@
    @ApiModelProperty(value = "单据明细")
    private List<OrderItem> children;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/OtherReceiptParams.java
@@ -28,3 +28,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/PublicToStockParams.java
@@ -20,3 +20,4 @@
    private List<WkOrderItem> itemList;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/QueryOrderParam.java
@@ -22,3 +22,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReceiptParams.java
@@ -27,3 +27,4 @@
    @ApiModelProperty(value = "库区标识", required = true)
    private Long whAreaId;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReportDataParam.java
@@ -63,3 +63,4 @@
    @ApiModelProperty("备注说明")
    private String MemoDtl;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/ReportParams.java
@@ -24,3 +24,4 @@
    private List<ReportDataParam> Data;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SaveCheckDiffParams.java
@@ -23,3 +23,4 @@
    @ApiModelProperty("差异单明细")
    private List<CheckDiffItem> checkDiffItems;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncCheckDiffParams.java
@@ -20,3 +20,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncLocReviseParams.java
@@ -28,3 +28,4 @@
    @ApiModelProperty("单据名称列表")
    private List<SyncReviseItems> reviseItems;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncLocsParams.java
@@ -80,3 +80,4 @@
    @ApiModelProperty(value= "状态 1: 正常  0: 冻结  ")
    private Integer status;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncMatGroupsParams.java
@@ -37,3 +37,4 @@
    private String code;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncReviseItems.java
@@ -22,3 +22,4 @@
    private List<SyncOrdersItem> items;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncTransferItems.java
@@ -50,3 +50,4 @@
    private String projectCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/SyncTransferParams.java
@@ -23,3 +23,4 @@
    @ApiModelProperty("调拔单明细")
    private List<SyncTransferItems> items;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/TaskInParam.java
@@ -34,3 +34,4 @@
//    private Integer locType2; //库位类型
//    private Integer locType3; //库位类型
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/WarehouseParams.java
@@ -31,3 +31,4 @@
    private String latitude;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/dto/CheckDiffDto.java
@@ -22,3 +22,4 @@
    @ApiModelProperty("差异单明细")
    List<CheckDiffItem> items;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/dto/TransferInfoDto.java
@@ -19,3 +19,4 @@
    private List<TransferItem> items;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/erp/params/dto/WkOrderDto.java
@@ -22,3 +22,4 @@
    private List<WkOrderItem> orderItems;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/mcp/AiMcpProtocolController.java
New file
@@ -0,0 +1,43 @@
package com.vincent.rsf.server.api.controller.mcp;
import com.fasterxml.jackson.databind.JsonNode;
import com.vincent.rsf.server.ai.service.mcp.AiMcpProtocolService;
import com.vincent.rsf.server.system.controller.BaseController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/ai/mcp")
public class AiMcpProtocolController extends BaseController {
    @Resource
    private AiMcpProtocolService aiMcpProtocolService;
    @PostMapping
    public Object handle(@RequestBody JsonNode body,
                         @RequestHeader(value = "X-Tenant-Id", required = false) String tenantHeader,
                         @RequestParam(value = "tenantId", required = false) Long tenantParam) {
        Long tenantId = resolveTenantId(tenantHeader, tenantParam);
        return aiMcpProtocolService.handle(tenantId, body);
    }
    private Long resolveTenantId(String tenantHeader, Long tenantParam) {
        if (tenantParam != null) {
            return tenantParam;
        }
        if (tenantHeader != null && !tenantHeader.trim().isEmpty()) {
            try {
                return Long.valueOf(tenantHeader.trim());
            } catch (Exception ignore) {
            }
        }
        Long loginTenantId = getTenantId();
        return loginTenantId == null ? 1L : loginTenantId;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/mcp/McpController.java
@@ -99,3 +99,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/mes/MesController.java
@@ -162,3 +162,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/AgvController.java
@@ -87,3 +87,4 @@
        return agvService.AGVBindAndInTaskStartT(param, getLoginUserId());
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/InBoundController.java
@@ -71,3 +71,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/MobileController.java
@@ -354,3 +354,4 @@
        return mobileService.generateTask(map, getLoginUserId());
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/MonitorController.java
@@ -83,3 +83,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaCheckOrderController.java
@@ -62,3 +62,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaOtherController.java
@@ -75,3 +75,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/PdaOutStockController.java
@@ -221,3 +221,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/pda/SysInfoController.java
@@ -67,3 +67,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/CommonResponse.java
@@ -18,5 +18,4 @@
    @ApiModelProperty("响应结果")
    private Object data;
}
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/constant/RcsConstant.java
@@ -19,3 +19,4 @@
    //待下发任务发送至中转站
    public static String MISSION_TRANSFER_STATION = "/rsf-open-api/mission/task/master/control";
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/CheckObjDto.java
@@ -43,3 +43,4 @@
    @ApiModelProperty("备注")
    private String memo;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/ContainerWaveDto.java
@@ -17,3 +17,4 @@
    private List<WkOrderItem> wkOrderItems;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/InTaskMsgDto.java
@@ -14,3 +14,4 @@
    private String workNo;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/InspectDetlDto.java
@@ -26,3 +26,4 @@
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/InspectItemDto.java
@@ -29,3 +29,4 @@
    @ApiModelProperty("供应商编码")
    private String suplierCode;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/LocTypeDto.java
@@ -19,3 +19,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/OperateStockDto.java
@@ -6,3 +6,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/PoItemsDto.java
@@ -41,3 +41,4 @@
    private String anfme;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/ReceiptDetlsDto.java
@@ -95,3 +95,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/SyncLocsDto.java
@@ -42,3 +42,4 @@
    private String status$;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/TaskLocAreaDto.java
@@ -23,3 +23,4 @@
    @ApiModelProperty("库位信息")
    private List<Loc> locs;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/TaskQueueDto.java
@@ -25,3 +25,4 @@
    private TaskLocAreaDto locArea;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/enums/CallBackEvent.java
@@ -23,3 +23,4 @@
    public String desc;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/enums/MatnrDefectType.java
@@ -24,3 +24,4 @@
    public Short type;
    public String val;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/enums/WcsMsgTypeEvent.java
@@ -21,3 +21,4 @@
    public String desc;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/CallForEmptyContainersParam.java
@@ -26,3 +26,4 @@
    private String taskNo;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ChangeLocParam.java
@@ -26,3 +26,4 @@
    private Integer locType1;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/CommonRequest.java
@@ -18,3 +18,4 @@
    @ApiModelProperty("条目数")
    private Integer pageSize;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ContainerWaveParam.java
@@ -16,3 +16,4 @@
    private List<ContainerWaveDto> containerWaveDtos;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/CreateInTaskParam.java
@@ -26,3 +26,4 @@
    private Integer locType1;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ErpInspectItem.java
@@ -36,3 +36,4 @@
     */
    public Double anfme;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ErpInspectParams.java
@@ -34,3 +34,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ExMsgParams.java
@@ -21,3 +21,4 @@
    @ApiModelProperty("容器码")
    private String zpallet;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/InTaskWcsReportParam.java
@@ -23,3 +23,4 @@
    private Integer taskPri;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/MissionTaskIssueParam.java
@@ -153,3 +153,4 @@
        this.retryTimes = flowStepInstance.getRetryTimes();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/OrderOutGeneralParam.java
@@ -27,3 +27,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/PdaGeneralParam.java
@@ -41,3 +41,4 @@
    private String sta2;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/ReassignLocParam.java
@@ -26,3 +26,4 @@
    private Integer locType1;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/TaskItemParam.java
@@ -34,3 +34,4 @@
    @ApiModelProperty("优先级")
    private Integer priority;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/TaskReportParam.java
@@ -41,3 +41,4 @@
    private Integer type = 0;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/params/WcsTaskParams.java
@@ -26,3 +26,4 @@
    @ApiModelProperty("任务明细")
    private List<TaskItemParam> taskList;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/validator/SyncOrderValidator.java
@@ -150,3 +150,4 @@
//                .sum();
//    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/AgvService.java
@@ -22,3 +22,4 @@
    R AGVBindAndInTaskStart(String barcode);
    boolean AGVBindAndInTaskStart(String barcode, String sta);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/MobileService.java
@@ -82,3 +82,4 @@
    R generateTask(Map<String, Object> map, Long loginUserId);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/MonitorService.java
@@ -8,3 +8,4 @@
    R getInOutHistories(Map<String, Object> param);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/PdaCheckOrderService.java
@@ -18,3 +18,4 @@
    R getCheckTaskItemList2(String barcode);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/PdaOtherService.java
@@ -21,3 +21,4 @@
    R locOperate(PdaGeneralParam generalParam, User loginUser);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/PdaOutStockService.java
@@ -22,3 +22,4 @@
    R taskItemList(PdaGeneralParam param, Long loginUserId);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/ReceiveMsgService.java
@@ -167,3 +167,4 @@
     */
    R erpQueryInventorySummary(InventoryQueryConditionParam condition);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/ReportMsgService.java
@@ -21,3 +21,4 @@
    R uploadCheckOrder(ReportParams params);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/WcsService.java
@@ -23,3 +23,4 @@
    R wcsReassignLoc(ReassignLocParam params);
    R wcsChangeLoc(ChangeLocParam params);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/AgvServiceImpl.java
@@ -580,3 +580,4 @@
        return true;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/InBoundServiceImpl.java
@@ -364,5 +364,4 @@
        }
        return R.ok();
    }
}
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java
@@ -1289,3 +1289,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MonitorServiceImpl.java
@@ -15,3 +15,4 @@
        return R.ok();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaCheckOrderServiceImpl.java
@@ -251,3 +251,4 @@
        return R.ok(Cools.add("checkDiffItems", checkDiffItems).add("checkDiff", checkDiff));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOtherServiceImpl.java
@@ -279,3 +279,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java
@@ -525,3 +525,4 @@
                item -> new BigDecimal(item.getAnfme().toString()).equals(new BigDecimal(item.getQty().toString())));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java
@@ -1490,3 +1490,4 @@
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReportMsgServiceImpl.java
@@ -259,3 +259,4 @@
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java
@@ -1204,3 +1204,4 @@
//        return R.ok(JSONObject.toJSONString(params));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/api/utils/LocUtils.java
@@ -487,3 +487,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/api/utils/SlaveProperties.java
@@ -26,3 +26,4 @@
    private int groupCount;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/utils/TimeConverterUtils.java
@@ -45,3 +45,4 @@
        return date.getTime();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/CodeBuilder.java
@@ -47,3 +47,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/annotation/OperationLog.java
@@ -39,3 +39,4 @@
    boolean result() default true;
}
rsf-server/src/main/java/com/vincent/rsf/server/common/aspect/OperationLogAspect.java
@@ -190,3 +190,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/BeanConfig.java
@@ -20,3 +20,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/ConfigProperties.java
@@ -84,3 +84,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/MybatisPlusConfig.java
@@ -117,3 +117,4 @@
        };
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/RedisProperties.java
@@ -94,3 +94,4 @@
        this.index = index;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/SchedulerConfig.java
@@ -21,3 +21,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/SwaggerConfig.java
@@ -102,3 +102,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/SysStockProperties.java
@@ -44,3 +44,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/WebMvcConfig.java
@@ -75,3 +75,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/config/WebSocketConfig.java
@@ -15,3 +15,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/constant/Constants.java
@@ -139,3 +139,4 @@
    public static final Integer TASK_SORT_MIN_VALUE =  0;
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/BaseParam.java
@@ -96,3 +96,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/BusinessRes.java
@@ -17,3 +17,4 @@
    public final static String EMAIL_EXIT = "10006 - Email address already exist";
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/KeyValVo.java
@@ -18,3 +18,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/PageParam.java
@@ -451,3 +451,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/PageResult.java
@@ -33,3 +33,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/QueueTask.java
@@ -5,3 +5,4 @@
@Data
public class QueueTask {
}
rsf-server/src/main/java/com/vincent/rsf/server/common/enums/WarehouseAreaType.java
@@ -23,3 +23,4 @@
    public String type;
    public String desc;
}
rsf-server/src/main/java/com/vincent/rsf/server/common/exception/BusinessException.java
@@ -47,3 +47,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java
@@ -101,3 +101,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/handler/AggregationDataHandler.java
@@ -67,3 +67,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/handler/ExcelDictHandlerImpl.java
@@ -47,3 +47,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/handler/global/GlobalDictService.java
@@ -134,3 +134,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/security/JwtAuthenticationFilter.java
@@ -115,3 +115,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/security/JwtSubject.java
@@ -29,3 +29,4 @@
    private Long tenantId;
}
rsf-server/src/main/java/com/vincent/rsf/server/common/security/SecurityConfig.java
@@ -57,6 +57,7 @@
            "/wcs/**",
            "/monitor/**",
            "/mcp/**",
            "/ai/mcp",
            "/mes/**"
    };
@@ -130,3 +131,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/service/EmailService.java
@@ -77,3 +77,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/service/RedisService.java
@@ -489,3 +489,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/CommonUtil.java
@@ -177,3 +177,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/DataFieldSortFunc.java
@@ -7,3 +7,4 @@
    List<String> getDataFieldSort();
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/DateUtils.java
@@ -744,3 +744,4 @@
        return newDate;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/ExcelUtil.java
@@ -225,3 +225,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/FileServerUtil.java
@@ -403,3 +403,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/Http.java
@@ -40,3 +40,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/IpTools.java
@@ -92,3 +92,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/JChardetFacadeUtil.java
@@ -2023,3 +2023,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/JSONUtil.java
@@ -74,3 +74,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/JwtUtil.java
@@ -141,3 +141,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/NodeUtils.java
@@ -38,3 +38,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/common/utils/OpenOfficeUtil.java
@@ -122,3 +122,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/CodeRes.java
@@ -16,3 +16,4 @@
    String SYSTEM_20001 = "20001-许可证已失效";
}
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/DictTypeCode.java
@@ -99,3 +99,4 @@
    /**库存调整*/
    public final static String SYS_STOCK_REVISE_TYPE =  "sys_stock_revise_type";
}
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/GlobalConfigCode.java
@@ -33,3 +33,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/SerialRuleCode.java
@@ -96,3 +96,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiCallLogController.java
New file
@@ -0,0 +1,103 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiCallLog;
import com.vincent.rsf.server.system.service.AiCallLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
public class AiCallLogController extends BaseController {
    @Autowired
    private AiCallLogService aiCallLogService;
    @PreAuthorize("hasAuthority('system:aiCallLog:list')")
    @PostMapping("/aiCallLog/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiCallLog, BaseParam> pageParam = new PageParam<>(baseParam, AiCallLog.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiCallLog> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiCallLogService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiCallLog:list')")
    @GetMapping("/aiCallLog/{id}")
    public R get(@PathVariable("id") Long id) {
        AiCallLog callLog = getTenantRecord(id);
        if (callLog == null) {
            return R.error("record not found");
        }
        return R.ok().add(callLog);
    }
    @PreAuthorize("hasAuthority('system:aiCallLog:list')")
    @PostMapping("/aiCallLog/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiCallLogService.list(new LambdaQueryWrapper<AiCallLog>()
                .eq(AiCallLog::getTenantId, getTenantId())), AiCallLog.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiCallLog:list')")
    @GetMapping("/ai/call-log/list")
    public R customList() {
        return R.ok().add(aiCallLogService.list(new LambdaQueryWrapper<AiCallLog>()
                .eq(AiCallLog::getTenantId, getTenantId())
                .orderByDesc(AiCallLog::getCreateTime, AiCallLog::getId)));
    }
    @PreAuthorize("hasAuthority('system:aiCallLog:list')")
    @GetMapping("/ai/call-log/stats")
    public R stats() {
        List<AiCallLog> logs = aiCallLogService.list(new LambdaQueryWrapper<AiCallLog>()
                .eq(AiCallLog::getTenantId, getTenantId())
                .orderByDesc(AiCallLog::getCreateTime, AiCallLog::getId));
        long total = logs.size();
        long successCount = logs.stream().filter(item -> Integer.valueOf(1).equals(item.getResult())).count();
        long failCount = logs.stream().filter(item -> Integer.valueOf(0).equals(item.getResult())).count();
        long modelCount = logs.stream().map(AiCallLog::getModelCode).filter(item -> item != null && !item.trim().isEmpty()).distinct().count();
        long routeCount = logs.stream().map(AiCallLog::getRouteCode).filter(item -> item != null && !item.trim().isEmpty()).distinct().count();
        long totalSpend = logs.stream().map(AiCallLog::getSpendTime).filter(item -> item != null && item > 0L).mapToLong(Long::longValue).sum();
        long spendCount = logs.stream().map(AiCallLog::getSpendTime).filter(item -> item != null && item > 0L).count();
        Date now = new Date();
        long last24hCount = logs.stream()
                .map(AiCallLog::getCreateTime)
                .filter(item -> item != null && now.getTime() - item.getTime() <= 24L * 60L * 60L * 1000L)
                .count();
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("total", total);
        payload.put("successCount", successCount);
        payload.put("failCount", failCount);
        payload.put("successRate", total <= 0 ? 0D : (successCount * 100D) / total);
        payload.put("avgSpendTime", spendCount <= 0 ? 0L : totalSpend / spendCount);
        payload.put("modelCount", modelCount);
        payload.put("routeCount", routeCount);
        payload.put("last24hCount", last24hCount);
        return R.ok().add(payload);
    }
    private AiCallLog getTenantRecord(Long id) {
        if (id == null) {
            return null;
        }
        return aiCallLogService.getOne(new LambdaQueryWrapper<AiCallLog>()
                .eq(AiCallLog::getTenantId, getTenantId())
                .eq(AiCallLog::getId, id)
                .last("limit 1"));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiDiagnosisController.java
New file
@@ -0,0 +1,97 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import com.vincent.rsf.server.system.service.AiDiagnosisRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
@RestController
public class AiDiagnosisController extends BaseController {
    @Autowired
    private AiDiagnosisRecordService aiDiagnosisRecordService;
    @PreAuthorize("hasAuthority('system:aiDiagnosis:list')")
    @PostMapping("/aiDiagnosis/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiDiagnosisRecord, BaseParam> pageParam = new PageParam<>(baseParam, AiDiagnosisRecord.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiDiagnosisRecord> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiDiagnosisRecordService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosis:list')")
    @GetMapping("/aiDiagnosis/{id}")
    public R get(@PathVariable("id") Long id) {
        AiDiagnosisRecord record = getTenantRecord(id);
        if (record == null) {
            return R.error("record not found");
        }
        return R.ok().add(record);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosis:list')")
    @PostMapping("/aiDiagnosis/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiDiagnosisRecordService.list(new LambdaQueryWrapper<AiDiagnosisRecord>()
                .eq(AiDiagnosisRecord::getTenantId, getTenantId())), AiDiagnosisRecord.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosis:list')")
    @GetMapping("/ai/diagnosis/list")
    public R diagnosisList() {
        return R.ok().add(aiDiagnosisRecordService.list(new LambdaQueryWrapper<AiDiagnosisRecord>()
                .eq(AiDiagnosisRecord::getTenantId, getTenantId())
                .orderByDesc(AiDiagnosisRecord::getCreateTime, AiDiagnosisRecord::getId)));
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosis:list')")
    @GetMapping("/ai/diagnosis/detail/{id}")
    public R detail(@PathVariable("id") Long id) {
        AiDiagnosisRecord record = getTenantRecord(id);
        if (record == null) {
            return R.error("record not found");
        }
        return R.ok().add(record);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosis:list')")
    @GetMapping("/ai/diagnosis/report/export/{id}")
    public void exportReport(@PathVariable("id") Long id, HttpServletResponse response) throws Exception {
        AiDiagnosisRecord record = getTenantRecord(id);
        if (record == null) {
            response.setStatus(404);
            return;
        }
        String fileName = URLEncoder.encode((record.getDiagnosisNo() == null ? "ai-diagnosis-report" : record.getDiagnosisNo()) + ".md", StandardCharsets.UTF_8.name());
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType("text/markdown; charset=utf-8");
        response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
        response.getWriter().write(record.getReportMarkdown() == null ? "" : record.getReportMarkdown());
        response.getWriter().flush();
    }
    private AiDiagnosisRecord getTenantRecord(Long id) {
        if (id == null) {
            return null;
        }
        return aiDiagnosisRecordService.getOne(new LambdaQueryWrapper<AiDiagnosisRecord>()
                .eq(AiDiagnosisRecord::getTenantId, getTenantId())
                .eq(AiDiagnosisRecord::getId, id)
                .last("limit 1"));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiDiagnosisPlanController.java
New file
@@ -0,0 +1,169 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.common.SnowflakeIdWorker;
import com.vincent.rsf.server.ai.service.diagnosis.AiDiagnosisPlanRunnerService;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiDiagnosisPlan;
import com.vincent.rsf.server.system.service.AiDiagnosisPlanService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
public class AiDiagnosisPlanController extends BaseController {
    @Autowired
    private AiDiagnosisPlanService aiDiagnosisPlanService;
    @Autowired
    private AiDiagnosisPlanRunnerService aiDiagnosisPlanRunnerService;
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;
    @Autowired
    private SnowflakeIdWorker snowflakeIdWorker;
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:list')")
    @PostMapping("/aiDiagnosisPlan/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiDiagnosisPlan, BaseParam> pageParam = new PageParam<>(baseParam, AiDiagnosisPlan.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiDiagnosisPlan> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiDiagnosisPlanService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:list')")
    @GetMapping("/aiDiagnosisPlan/{id}")
    public R get(@PathVariable("id") Long id) {
        AiDiagnosisPlan plan = aiDiagnosisPlanService.getTenantPlan(getTenantId(), id);
        if (plan == null) {
            return R.error("plan not found");
        }
        return R.ok().add(plan);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:save')")
    @OperationLog("Create AiDiagnosisPlan")
    @PostMapping("/aiDiagnosisPlan/save")
    public R save(@RequestBody AiDiagnosisPlan plan) {
        if (Cools.isEmpty(plan.getPlanName()) || Cools.isEmpty(plan.getCronExpr())) {
            return R.error("计划名称和Cron表达式不能为空");
        }
        if (!aiDiagnosisPlanService.validateCron(plan.getCronExpr())) {
            return R.error("Cron表达式不合法");
        }
        Date now = new Date();
        plan.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
        plan.setTenantId(getTenantId());
        plan.setSceneCode(Cools.isEmpty(plan.getSceneCode()) ? "system_diagnose" : plan.getSceneCode());
        plan.setRunningFlag(0);
        plan.setStatus(plan.getStatus() == null ? 1 : plan.getStatus());
        plan.setNextRunTime(Integer.valueOf(1).equals(plan.getStatus())
                ? aiDiagnosisPlanService.calculateNextRunTime(plan.getCronExpr(), now)
                : null);
        plan.setCreateBy(getLoginUserId());
        plan.setCreateTime(now);
        plan.setUpdateBy(getLoginUserId());
        plan.setUpdateTime(now);
        if (!aiDiagnosisPlanService.save(plan)) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(plan);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:update')")
    @OperationLog("Update AiDiagnosisPlan")
    @PostMapping("/aiDiagnosisPlan/update")
    public R update(@RequestBody AiDiagnosisPlan plan) {
        AiDiagnosisPlan existed = aiDiagnosisPlanService.getTenantPlan(getTenantId(), plan.getId());
        if (existed == null) {
            return R.error("plan not found");
        }
        if (Cools.isEmpty(plan.getPlanName()) || Cools.isEmpty(plan.getCronExpr())) {
            return R.error("计划名称和Cron表达式不能为空");
        }
        if (!aiDiagnosisPlanService.validateCron(plan.getCronExpr())) {
            return R.error("Cron表达式不合法");
        }
        plan.setTenantId(getTenantId());
        plan.setSceneCode(Cools.isEmpty(plan.getSceneCode()) ? existed.getSceneCode() : plan.getSceneCode());
        plan.setRunningFlag(existed.getRunningFlag());
        plan.setLastResult(existed.getLastResult());
        plan.setLastDiagnosisId(existed.getLastDiagnosisId());
        plan.setLastRunTime(existed.getLastRunTime());
        plan.setLastMessage(existed.getLastMessage());
        plan.setNextRunTime(Integer.valueOf(1).equals(plan.getStatus())
                ? aiDiagnosisPlanService.calculateNextRunTime(plan.getCronExpr(), new Date())
                : null);
        plan.setCreateBy(existed.getCreateBy());
        plan.setCreateTime(existed.getCreateTime());
        plan.setUpdateBy(getLoginUserId());
        plan.setUpdateTime(new Date());
        if (!aiDiagnosisPlanService.updateById(plan)) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(plan);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:remove')")
    @OperationLog("Delete AiDiagnosisPlan")
    @PostMapping("/aiDiagnosisPlan/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        List<AiDiagnosisPlan> plans = aiDiagnosisPlanService.list(new LambdaQueryWrapper<AiDiagnosisPlan>()
                .eq(AiDiagnosisPlan::getTenantId, getTenantId())
                .in(AiDiagnosisPlan::getId, idList));
        if (plans.size() != idList.size() || !aiDiagnosisPlanService.removeByIds(idList)) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:list')")
    @PostMapping("/aiDiagnosisPlan/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiDiagnosisPlanService.list(new LambdaQueryWrapper<AiDiagnosisPlan>()
                .eq(AiDiagnosisPlan::getTenantId, getTenantId())), AiDiagnosisPlan.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiDiagnosisPlan:update')")
    @OperationLog("Run AiDiagnosisPlan")
    @PostMapping("/ai/diagnosis-plan/run")
    public R run(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        AiDiagnosisPlan plan = aiDiagnosisPlanService.getTenantPlan(getTenantId(), id);
        if (plan == null) {
            return R.error("plan not found");
        }
        if (Integer.valueOf(1).equals(plan.getRunningFlag())) {
            return R.error("计划正在执行中");
        }
        Date nextRunTime = Integer.valueOf(1).equals(plan.getStatus())
                ? aiDiagnosisPlanService.calculateNextRunTime(plan.getCronExpr(), new Date())
                : null;
        boolean acquired = aiDiagnosisPlanService.acquireForExecution(
                plan.getId(),
                getLoginUserId(),
                "手动执行中",
                nextRunTime
        );
        if (!acquired) {
            return R.error("计划正在执行中");
        }
        taskScheduler.execute(() -> aiDiagnosisPlanRunnerService.runPlan(plan.getId(), true));
        return R.ok();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiDiagnosticToolConfigController.java
New file
@@ -0,0 +1,156 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.common.SnowflakeIdWorker;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
public class AiDiagnosticToolConfigController extends BaseController {
    @Autowired
    private AiDiagnosticToolConfigService aiDiagnosticToolConfigService;
    @Autowired
    private SnowflakeIdWorker snowflakeIdWorker;
    @PreAuthorize("hasAuthority('system:aiToolConfig:list')")
    @PostMapping("/aiToolConfig/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiDiagnosticToolConfig, BaseParam> pageParam = new PageParam<>(baseParam, AiDiagnosticToolConfig.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiDiagnosticToolConfig> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiDiagnosticToolConfigService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiToolConfig:list')")
    @GetMapping("/aiToolConfig/{id}")
    public R get(@PathVariable("id") Long id) {
        AiDiagnosticToolConfig config = aiDiagnosticToolConfigService.getTenantConfig(getTenantId(), id);
        if (config == null) {
            return R.error("config not found");
        }
        return R.ok().add(config);
    }
    @PreAuthorize("hasAuthority('system:aiToolConfig:save')")
    @OperationLog("Create AiDiagnosticToolConfig")
    @PostMapping("/aiToolConfig/save")
    public R save(@RequestBody AiDiagnosticToolConfig config) {
        if (Cools.isEmpty(config.getSceneCode()) || Cools.isEmpty(config.getToolCode())) {
            return R.error("场景编码和工具编码不能为空");
        }
        Date now = new Date();
        config.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
        config.setTenantId(getTenantId());
        config.setCreateBy(getLoginUserId());
        config.setCreateTime(now);
        config.setUpdateBy(getLoginUserId());
        config.setUpdateTime(now);
        if (config.getEnabledFlag() == null) {
            config.setEnabledFlag(1);
        }
        if (config.getPriority() == null) {
            config.setPriority(1);
        }
        if (config.getStatus() == null) {
            config.setStatus(1);
        }
        if (!aiDiagnosticToolConfigService.save(config)) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(config);
    }
    @PreAuthorize("hasAuthority('system:aiToolConfig:update')")
    @OperationLog("Update AiDiagnosticToolConfig")
    @PostMapping("/aiToolConfig/update")
    public R update(@RequestBody AiDiagnosticToolConfig config) {
        AiDiagnosticToolConfig existed = aiDiagnosticToolConfigService.getTenantConfig(getTenantId(), config.getId());
        if (existed == null) {
            return R.error("config not found");
        }
        config.setTenantId(getTenantId());
        config.setUpdateBy(getLoginUserId());
        config.setUpdateTime(new Date());
        config.setCreateBy(existed.getCreateBy());
        config.setCreateTime(existed.getCreateTime());
        if (config.getEnabledFlag() == null) {
            config.setEnabledFlag(existed.getEnabledFlag());
        }
        if (config.getPriority() == null) {
            config.setPriority(existed.getPriority());
        }
        if (config.getStatus() == null) {
            config.setStatus(existed.getStatus());
        }
        if (!aiDiagnosticToolConfigService.updateById(config)) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(config);
    }
    @PreAuthorize("hasAuthority('system:aiToolConfig:remove')")
    @OperationLog("Delete AiDiagnosticToolConfig")
    @PostMapping("/aiToolConfig/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        List<AiDiagnosticToolConfig> configs = aiDiagnosticToolConfigService.list(new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, getTenantId())
                .in(AiDiagnosticToolConfig::getId, idList));
        if (configs.size() != idList.size() || !aiDiagnosticToolConfigService.removeByIds(idList)) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:aiToolConfig:list')")
    @PostMapping("/aiToolConfig/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiDiagnosticToolConfigService.list(new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, getTenantId())), AiDiagnosticToolConfig.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiToolConfig:list')")
    @GetMapping("/ai/tool-config/list")
    public R customList(@RequestParam(required = false) String sceneCode) {
        LambdaQueryWrapper<AiDiagnosticToolConfig> wrapper = new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, getTenantId())
                .orderByAsc(AiDiagnosticToolConfig::getSceneCode, AiDiagnosticToolConfig::getPriority, AiDiagnosticToolConfig::getId);
        if (!Cools.isEmpty(sceneCode)) {
            wrapper.eq(AiDiagnosticToolConfig::getSceneCode, sceneCode);
        }
        return R.ok().add(aiDiagnosticToolConfigService.list(wrapper));
    }
    @PreAuthorize("hasAnyAuthority('system:aiToolConfig:save','system:aiToolConfig:update')")
    @PostMapping("/ai/tool-config/save")
    public R customSave(@RequestBody AiDiagnosticToolConfig config) {
        if (config.getId() == null) {
            if (!hasAuthority("system:aiToolConfig:save")) {
                return R.error("无新增权限");
            }
            return save(config);
        }
        if (!hasAuthority("system:aiToolConfig:update")) {
            return R.error("无更新权限");
        }
        return update(config);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiMcpMountController.java
New file
@@ -0,0 +1,394 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.common.SnowflakeIdWorker;
import com.vincent.rsf.server.ai.constant.AiMcpConstants;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.ai.service.mcp.AiMcpPayloadMapper;
import com.vincent.rsf.server.ai.service.mcp.AiMcpRegistryService;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService;
import com.vincent.rsf.server.system.service.AiMcpMountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
public class AiMcpMountController extends BaseController {
    @Autowired
    private AiMcpMountService aiMcpMountService;
    @Autowired
    private AiMcpRegistryService aiMcpRegistryService;
    @Autowired
    private AiDiagnosticToolConfigService aiDiagnosticToolConfigService;
    @Autowired
    private SnowflakeIdWorker snowflakeIdWorker;
    @Autowired
    private AiMcpPayloadMapper aiMcpPayloadMapper;
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @PostMapping("/aiMcpMount/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiMcpMount, BaseParam> pageParam = new PageParam<>(baseParam, AiMcpMount.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiMcpMount> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiMcpMountService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @GetMapping("/aiMcpMount/{id}")
    public R get(@PathVariable("id") Long id) {
        AiMcpMount mount = aiMcpMountService.getTenantMount(getTenantId(), id);
        if (mount == null) {
            return R.error("mount not found");
        }
        return R.ok().add(mount);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:save')")
    @OperationLog("Create AiMcpMount")
    @PostMapping("/aiMcpMount/save")
    public R save(@RequestBody AiMcpMount mount) {
        if (Cools.isEmpty(mount.getName()) || Cools.isEmpty(mount.getMountCode()) || Cools.isEmpty(mount.getTransportType())) {
            return R.error("名称、挂载编码和传输类型不能为空");
        }
        Date now = new Date();
        mount.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
        mount.setEnabledFlag(mount.getEnabledFlag() == null ? 1 : mount.getEnabledFlag());
        mount.setTimeoutMs(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs());
        mount.setStatus(mount.getStatus() == null ? 1 : mount.getStatus());
        mount.setTransportType(mount.getTransportType().toUpperCase());
        if (AiMcpConstants.TRANSPORT_INTERNAL.equals(mount.getTransportType())) {
            mount.setUrl("/ai/mcp");
        }
        mount.setTenantId(getTenantId());
        mount.setCreateBy(getLoginUserId());
        mount.setCreateTime(now);
        mount.setUpdateBy(getLoginUserId());
        mount.setUpdateTime(now);
        if (!aiMcpMountService.save(mount)) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(mount);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
    @OperationLog("Update AiMcpMount")
    @PostMapping("/aiMcpMount/update")
    public R update(@RequestBody AiMcpMount mount) {
        AiMcpMount existed = aiMcpMountService.getTenantMount(getTenantId(), mount.getId());
        if (existed == null) {
            return R.error("mount not found");
        }
        if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(existed.getTransportType())) {
            return R.error("内置挂载由系统托管,不支持直接编辑");
        }
        if (Cools.isEmpty(mount.getName()) || Cools.isEmpty(mount.getMountCode()) || Cools.isEmpty(mount.getTransportType())) {
            return R.error("名称、挂载编码和传输类型不能为空");
        }
        mount.setTransportType(mount.getTransportType().toUpperCase());
        if (AiMcpConstants.TRANSPORT_INTERNAL.equals(mount.getTransportType())) {
            mount.setUrl("/ai/mcp");
        }
        mount.setTenantId(getTenantId());
        mount.setCreateBy(existed.getCreateBy());
        mount.setCreateTime(existed.getCreateTime());
        mount.setLastTestResult(existed.getLastTestResult());
        mount.setLastTestTime(existed.getLastTestTime());
        mount.setLastTestMessage(existed.getLastTestMessage());
        mount.setLastToolCount(existed.getLastToolCount());
        mount.setUpdateBy(getLoginUserId());
        mount.setUpdateTime(new Date());
        if (!aiMcpMountService.updateById(mount)) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(mount);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:remove')")
    @OperationLog("Delete AiMcpMount")
    @PostMapping("/aiMcpMount/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        List<AiMcpMount> mounts = aiMcpMountService.list(new LambdaQueryWrapper<AiMcpMount>()
                .eq(AiMcpMount::getTenantId, getTenantId())
                .in(AiMcpMount::getId, idList));
        for (AiMcpMount mount : mounts) {
            if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) {
                return R.error("内置挂载由系统托管,不支持删除");
            }
        }
        if (mounts.size() != idList.size() || !aiMcpMountService.removeByIds(idList)) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @PostMapping("/aiMcpMount/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiMcpMountService.list(new LambdaQueryWrapper<AiMcpMount>()
                .eq(AiMcpMount::getTenantId, getTenantId())), AiMcpMount.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @GetMapping("/ai/mcp/mount/list")
    public R list() {
        aiMcpRegistryService.ensureDefaultMount(getTenantId(), getLoginUserId());
        return R.ok().add(aiMcpMountService.listTenantMounts(getTenantId()));
    }
    @PreAuthorize("hasAnyAuthority('system:aiMcpMount:save','system:aiMcpMount:update')")
    @PostMapping("/ai/mcp/mount/save")
    public R customSave(@RequestBody AiMcpMount mount) {
        if (mount.getId() == null) {
            return save(mount);
        }
        return update(mount);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
    @PostMapping("/ai/mcp/mount/initDefaults")
    public R initDefaults() {
        aiMcpRegistryService.ensureDefaultMount(getTenantId(), getLoginUserId());
        return R.ok();
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @GetMapping("/ai/mcp/mount/toolList")
    public R toolList(@RequestParam(required = false) Long mountId) {
        return R.ok().add(aiMcpRegistryService.listTools(getTenantId(), mountId));
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
    @PostMapping("/ai/mcp/mount/test")
    public R test(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        return R.ok().add(aiMcpRegistryService.testMount(getTenantId(), id));
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @PostMapping("/ai/mcp/mount/toolPreview")
    public R toolPreview(@RequestBody Map<String, Object> map) {
        return R.ok().add(aiMcpRegistryService.previewTool(
                getTenantId(),
                String.valueOf(map.get("mountCode")),
                String.valueOf(map.get("toolCode")),
                map.get("sceneCode") == null ? null : String.valueOf(map.get("sceneCode")),
                map.get("question") == null ? null : String.valueOf(map.get("question"))
        ));
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:list')")
    @GetMapping("/ai/mcp/console/overview")
    public R consoleOverview() {
        aiMcpRegistryService.ensureDefaultMount(getTenantId(), getLoginUserId());
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("builtInMount", null);
        payload.put("builtInTools", new java.util.ArrayList<>());
        payload.put("externalServices", new java.util.ArrayList<>());
        for (AiMcpMount mount : aiMcpMountService.listTenantMounts(getTenantId())) {
            if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) {
                payload.put("builtInMount", buildMountView(mount, true));
                payload.put("builtInTools", buildBuiltInToolViews());
            } else {
                ((List<Object>) payload.get("externalServices")).add(buildMountView(mount, false));
            }
        }
        return R.ok().add(payload);
    }
    @PreAuthorize("hasAnyAuthority('system:aiMcpMount:save','system:aiMcpMount:update')")
    @PostMapping("/ai/mcp/console/service/save")
    public R consoleSaveService(@RequestBody AiMcpMount mount) {
        if (Cools.isEmpty(mount.getName()) || Cools.isEmpty(mount.getUrl())) {
            return R.error("名称和地址不能为空");
        }
        Date now = new Date();
        AiMcpMount existed = mount.getId() == null ? null : aiMcpMountService.getTenantMount(getTenantId(), mount.getId());
        if (existed != null && AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(existed.getTransportType())) {
            return R.error("内置挂载由系统托管,不支持编辑");
        }
        String transportType = Cools.isEmpty(mount.getTransportType()) ? AiMcpConstants.TRANSPORT_AUTO : mount.getTransportType().toUpperCase();
        String usageScope = normalizeUsageScope(mount.getUsageScope());
        AiMcpMount target = existed == null ? new AiMcpMount() : existed;
        target.setName(mount.getName());
        target.setUrl(mount.getUrl());
        target.setTransportType(transportType);
        target.setAuthType(Cools.isEmpty(mount.getAuthType()) ? AiMcpConstants.AUTH_TYPE_NONE : mount.getAuthType().toUpperCase());
        target.setAuthValue(mount.getAuthValue());
        target.setUsageScope(usageScope);
        target.setEnabledFlag(mount.getEnabledFlag() == null ? 1 : mount.getEnabledFlag());
        target.setTimeoutMs(mount.getTimeoutMs() == null ? 10000 : mount.getTimeoutMs());
        target.setStatus(mount.getStatus() == null ? 1 : mount.getStatus());
        target.setMemo(mount.getMemo());
        target.setTenantId(getTenantId());
        target.setUpdateBy(getLoginUserId());
        target.setUpdateTime(now);
        if (existed == null) {
            target.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
            target.setMountCode(generateMountCode(target.getName()));
            target.setCreateBy(getLoginUserId());
            target.setCreateTime(now);
            if (!aiMcpMountService.save(target)) {
                return R.error("Save Fail");
            }
        } else if (!aiMcpMountService.updateById(target)) {
            return R.error("Update Fail");
        }
        return R.ok().add(buildMountView(target, false));
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:update')")
    @PostMapping("/ai/mcp/console/service/test")
    public R consoleTestService(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        Map<String, Object> result = (Map<String, Object>) aiMcpRegistryService.testMount(getTenantId(), id);
        AiMcpMount mount = aiMcpMountService.getTenantMount(getTenantId(), id);
        if (mount == null) {
            return R.error("mount not found");
        }
        result.put("service", buildMountView(mount, false));
        return R.ok().add(result);
    }
    @PreAuthorize("hasAuthority('system:aiMcpMount:remove')")
    @PostMapping("/ai/mcp/console/service/remove/{id}")
    public R consoleRemoveService(@PathVariable("id") Long id) {
        AiMcpMount mount = aiMcpMountService.getTenantMount(getTenantId(), id);
        if (mount == null) {
            return R.error("mount not found");
        }
        if (AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType())) {
            return R.error("内置挂载由系统托管,不支持删除");
        }
        return aiMcpMountService.removeById(id) ? R.ok() : R.error("Delete Fail");
    }
    @PreAuthorize("hasAnyAuthority('system:aiToolConfig:save','system:aiToolConfig:update')")
    @PostMapping("/ai/mcp/console/builtin-tool/save")
    public R consoleSaveBuiltInTool(@RequestBody AiDiagnosticToolConfig config) {
        if (Cools.isEmpty(config.getToolCode())) {
            return R.error("工具编码不能为空");
        }
        String usageScope = aiMcpPayloadMapper.normalizeUsageScope(config.getUsageScope());
        String sceneCode = resolveSceneCodeByUsageScope(usageScope);
        LambdaQueryWrapper<AiDiagnosticToolConfig> wrapper = new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, getTenantId())
                .eq(AiDiagnosticToolConfig::getToolCode, config.getToolCode());
        if (Cools.isEmpty(sceneCode)) {
            wrapper.and(w -> w.isNull(AiDiagnosticToolConfig::getSceneCode).or().eq(AiDiagnosticToolConfig::getSceneCode, ""));
        } else {
            wrapper.eq(AiDiagnosticToolConfig::getSceneCode, sceneCode);
        }
        AiDiagnosticToolConfig existed = aiDiagnosticToolConfigService.getOne(wrapper.last("limit 1"));
        Date now = new Date();
        AiDiagnosticToolConfig target = existed == null ? new AiDiagnosticToolConfig() : existed;
        if (existed == null) {
            target.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
            target.setCreateBy(getLoginUserId());
            target.setCreateTime(now);
        }
        target
                .setToolCode(config.getToolCode())
                .setToolName(config.getToolName())
                .setSceneCode(sceneCode)
                .setUsageScope(usageScope)
                .setEnabledFlag(AiMcpConstants.USAGE_SCOPE_DISABLED.equals(usageScope) ? 0 : (config.getEnabledFlag() == null ? 1 : config.getEnabledFlag()))
                .setPriority(config.getPriority() == null ? 10 : config.getPriority())
                .setToolPrompt(config.getToolPrompt())
                .setStatus(config.getStatus() == null ? 1 : config.getStatus())
                .setTenantId(getTenantId())
                .setUpdateBy(getLoginUserId())
                .setUpdateTime(now)
                .setMemo(config.getMemo());
        if (existed == null) {
            aiDiagnosticToolConfigService.save(target);
        } else {
            aiDiagnosticToolConfigService.updateById(target);
        }
        return R.ok().add(buildBuiltInToolViews());
    }
    private List<Map<String, Object>> buildBuiltInToolViews() {
        List<Map<String, Object>> output = new java.util.ArrayList<>();
        for (com.vincent.rsf.server.ai.model.AiMcpToolDescriptor descriptor : aiMcpRegistryService.listInternalTools(getTenantId())) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("toolCode", descriptor.getToolCode());
            item.put("toolName", descriptor.getToolName());
            item.put("description", descriptor.getDescription());
            item.put("enabledFlag", descriptor.getEnabledFlag());
            item.put("priority", descriptor.getPriority());
            item.put("toolPrompt", descriptor.getToolPrompt());
            item.put("usageScope", aiMcpPayloadMapper.resolveUsageScope(descriptor.getSceneCode(), descriptor.getEnabledFlag(), descriptor.getUsageScope()));
            item.put("advanced", buildAdvancedFlag(descriptor));
            output.add(item);
        }
        return output;
    }
    private Map<String, Object> buildMountView(AiMcpMount mount, boolean internalManaged) {
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("id", mount.getId());
        item.put("name", mount.getName());
        item.put("mountCode", mount.getMountCode());
        item.put("transportType", mount.getTransportType());
        item.put("displayTransportType", AiMcpConstants.TRANSPORT_INTERNAL.equalsIgnoreCase(mount.getTransportType()) ? "内置" : mount.getTransportType());
        item.put("url", mount.getUrl());
        item.put("enabledFlag", mount.getEnabledFlag());
        item.put("timeoutMs", mount.getTimeoutMs());
        item.put("lastTestResult", mount.getLastTestResult());
        item.put("lastTestResult$", mount.getLastTestResult$());
        item.put("lastTestTime", mount.getLastTestTime$());
        item.put("lastTestMessage", mount.getLastTestMessage());
        item.put("lastToolCount", mount.getLastToolCount());
        item.put("authType", mount.getAuthType());
        item.put("hasAuth", !Cools.isEmpty(mount.getAuthValue()));
        item.put("usageScope", normalizeUsageScope(mount.getUsageScope()));
        item.put("internalManaged", internalManaged);
        return item;
    }
    private boolean buildAdvancedFlag(com.vincent.rsf.server.ai.model.AiMcpToolDescriptor descriptor) {
        return descriptor.getPriority() != null && descriptor.getPriority() != 10
                || (descriptor.getToolPrompt() != null && !descriptor.getToolPrompt().trim().isEmpty());
    }
    private String normalizeUsageScope(String usageScope) {
        return aiMcpPayloadMapper.normalizeUsageScope(usageScope);
    }
    private String resolveUsageScope(String sceneCode, Integer enabledFlag) {
        return aiMcpPayloadMapper.resolveUsageScope(sceneCode, enabledFlag, null);
    }
    private String resolveSceneCodeByUsageScope(String usageScope) {
        return AiMcpConstants.USAGE_SCOPE_DIAGNOSE_ONLY.equals(usageScope) ? AiSceneCode.SYSTEM_DIAGNOSE : "";
    }
    private String generateMountCode(String name) {
        String normalized = name == null ? "remote_mcp" : name.toLowerCase().replaceAll("[^a-z0-9]+", "_");
        normalized = normalized.replaceAll("^_+|_+$", "");
        if (normalized.isEmpty()) {
            normalized = "remote_mcp";
        }
        return normalized + "_" + String.valueOf(System.currentTimeMillis()).substring(7);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiParamController.java
@@ -136,3 +136,4 @@
        ExcelUtil.build(ExcelUtil.create(aiParamService.list(), AiParam.class), response);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiPromptController.java
New file
@@ -0,0 +1,233 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.common.SnowflakeIdWorker;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiPromptPublishLog;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import com.vincent.rsf.server.system.service.AiPromptPublishLogService;
import com.vincent.rsf.server.system.service.AiPromptTemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
public class AiPromptController extends BaseController {
    @Autowired
    private AiPromptTemplateService aiPromptTemplateService;
    @Autowired
    private AiPromptPublishLogService aiPromptPublishLogService;
    @Autowired
    private SnowflakeIdWorker snowflakeIdWorker;
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @PostMapping("/aiPrompt/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiPromptTemplate, BaseParam> pageParam = new PageParam<>(baseParam, AiPromptTemplate.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiPromptTemplate> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiPromptTemplateService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @PostMapping("/aiPrompt/list")
    public R list(@RequestBody(required = false) Map<String, Object> map) {
        return R.ok().add(aiPromptTemplateService.list(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, getTenantId())
                .orderByAsc(AiPromptTemplate::getSceneCode)
                .orderByDesc(AiPromptTemplate::getVersionNo, AiPromptTemplate::getId)));
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @PostMapping({"/aiPrompt/many/{ids}", "/aiPrompts/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(aiPromptTemplateService.list(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, getTenantId())
                .in(AiPromptTemplate::getId, Arrays.asList(ids))));
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @GetMapping("/aiPrompt/{id}")
    public R get(@PathVariable("id") Long id) {
        AiPromptTemplate promptTemplate = aiPromptTemplateService.getTenantTemplate(getTenantId(), id);
        if (promptTemplate == null) {
            return R.error("record not found");
        }
        return R.ok().add(promptTemplate);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:save')")
    @OperationLog("Create AiPrompt")
    @PostMapping("/aiPrompt/save")
    public R save(@RequestBody AiPromptTemplate aiPromptTemplate) {
        if (Cools.isEmpty(aiPromptTemplate.getSceneCode())) {
            return R.error("场景编码不能为空");
        }
        Date now = new Date();
        aiPromptTemplate.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
        aiPromptTemplate.setTenantId(getTenantId());
        aiPromptTemplate.setCreateBy(getLoginUserId());
        aiPromptTemplate.setCreateTime(now);
        aiPromptTemplate.setUpdateBy(getLoginUserId());
        aiPromptTemplate.setUpdateTime(now);
        aiPromptTemplate.setVersionNo(aiPromptTemplate.getVersionNo() == null
                ? aiPromptTemplateService.nextVersionNo(getTenantId(), aiPromptTemplate.getSceneCode())
                : aiPromptTemplate.getVersionNo());
        if (aiPromptTemplate.getPublishedFlag() == null) {
            aiPromptTemplate.setPublishedFlag(0);
        }
        if (aiPromptTemplate.getStatus() == null) {
            aiPromptTemplate.setStatus(1);
        }
        if (!aiPromptTemplateService.save(aiPromptTemplate)) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(aiPromptTemplate);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:update')")
    @OperationLog("Update AiPrompt")
    @PostMapping("/aiPrompt/update")
    public R update(@RequestBody AiPromptTemplate aiPromptTemplate) {
        AiPromptTemplate existed = aiPromptTemplateService.getTenantTemplate(getTenantId(), aiPromptTemplate.getId());
        if (existed == null) {
            return R.error("record not found");
        }
        aiPromptTemplate.setTenantId(getTenantId());
        aiPromptTemplate.setUpdateBy(getLoginUserId());
        aiPromptTemplate.setUpdateTime(new Date());
        aiPromptTemplate.setCreateBy(existed.getCreateBy());
        aiPromptTemplate.setCreateTime(existed.getCreateTime());
        if (!aiPromptTemplateService.updateById(aiPromptTemplate)) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(aiPromptTemplate);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:remove')")
    @OperationLog("Delete AiPrompt")
    @PostMapping("/aiPrompt/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        List<AiPromptTemplate> records = aiPromptTemplateService.list(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, getTenantId())
                .in(AiPromptTemplate::getId, idList));
        if (records.size() != idList.size() || !aiPromptTemplateService.removeByIds(idList)) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @PostMapping("/aiPrompt/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiPromptTemplateService.list(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, getTenantId())), AiPromptTemplate.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @GetMapping("/ai/prompt/list")
    public R customList(@RequestParam(required = false) String sceneCode) {
        LambdaQueryWrapper<AiPromptTemplate> wrapper = new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, getTenantId())
                .orderByAsc(AiPromptTemplate::getSceneCode)
                .orderByDesc(AiPromptTemplate::getVersionNo, AiPromptTemplate::getId);
        if (!Cools.isEmpty(sceneCode)) {
            wrapper.eq(AiPromptTemplate::getSceneCode, sceneCode);
        }
        return R.ok().add(aiPromptTemplateService.list(wrapper));
    }
    @PreAuthorize("hasAnyAuthority('system:aiPrompt:save','system:aiPrompt:update')")
    @PostMapping("/ai/prompt/save")
    public R customSave(@RequestBody AiPromptTemplate aiPromptTemplate) {
        if (aiPromptTemplate.getId() == null) {
            if (!hasAuthority("system:aiPrompt:save")) {
                return R.error("无新增权限");
            }
            return save(aiPromptTemplate);
        }
        if (!hasAuthority("system:aiPrompt:update")) {
            return R.error("无更新权限");
        }
        return update(aiPromptTemplate);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:publish')")
    @OperationLog("Publish AiPrompt")
    @PostMapping("/ai/prompt/publish")
    public R publish(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        if (!aiPromptTemplateService.publishTemplate(getTenantId(), id, getLoginUserId())) {
            return R.error("record not found");
        }
        return R.ok();
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @GetMapping("/ai/prompt/version/list")
    public R versionList(@RequestParam String sceneCode) {
        List<AiPromptTemplate> list = aiPromptTemplateService.listVersions(getTenantId(), sceneCode);
        return R.ok().add(list);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @GetMapping("/ai/prompt/publish-log/list")
    public R publishLogList(@RequestParam(required = false) String sceneCode) {
        List<AiPromptPublishLog> list = aiPromptPublishLogService.listSceneLogs(getTenantId(), sceneCode);
        return R.ok().add(list);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:save')")
    @OperationLog("Copy AiPrompt")
    @PostMapping("/ai/prompt/copy")
    public R copy(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        AiPromptTemplate copied = aiPromptTemplateService.copyTemplate(getTenantId(), id, getLoginUserId());
        if (copied == null) {
            return R.error("record not found");
        }
        return R.ok().add(copied);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:list')")
    @GetMapping("/ai/prompt/compare")
    public R compare(@RequestParam Long leftId, @RequestParam Long rightId) {
        AiPromptTemplate left = aiPromptTemplateService.getTenantTemplate(getTenantId(), leftId);
        AiPromptTemplate right = aiPromptTemplateService.getTenantTemplate(getTenantId(), rightId);
        if (left == null || right == null) {
            return R.error("record not found");
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("left", left);
        result.put("right", right);
        return R.ok().add(result);
    }
    @PreAuthorize("hasAuthority('system:aiPrompt:publish')")
    @OperationLog("Rollback AiPrompt")
    @PostMapping("/ai/prompt/rollback")
    public R rollback(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        if (!aiPromptTemplateService.rollbackTemplate(getTenantId(), id, getLoginUserId())) {
            return R.error("record not found");
        }
        return R.ok();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AiRouteController.java
New file
@@ -0,0 +1,183 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.common.SnowflakeIdWorker;
import com.vincent.rsf.server.ai.service.AiModelRouteRuntimeService;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.system.entity.AiModelRoute;
import com.vincent.rsf.server.system.service.AiModelRouteService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
public class AiRouteController extends BaseController {
    @Autowired
    private AiModelRouteService aiModelRouteService;
    @Autowired
    private AiModelRouteRuntimeService aiModelRouteRuntimeService;
    @Autowired
    private SnowflakeIdWorker snowflakeIdWorker;
    @PreAuthorize("hasAuthority('system:aiRoute:list')")
    @PostMapping("/aiRoute/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AiModelRoute, BaseParam> pageParam = new PageParam<>(baseParam, AiModelRoute.class);
        com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<AiModelRoute> wrapper = pageParam.buildWrapper(true);
        wrapper.eq("tenant_id", getTenantId());
        return R.ok().add(aiModelRouteService.page(pageParam, wrapper));
    }
    @PreAuthorize("hasAuthority('system:aiRoute:list')")
    @GetMapping("/aiRoute/{id}")
    public R get(@PathVariable("id") Long id) {
        AiModelRoute route = aiModelRouteService.getTenantRoute(getTenantId(), id);
        if (route == null) {
            return R.error("route not found");
        }
        return R.ok().add(route);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:save')")
    @OperationLog("Create AiRoute")
    @PostMapping("/aiRoute/save")
    public R save(@RequestBody AiModelRoute aiModelRoute) {
        if (Cools.isEmpty(aiModelRoute.getRouteCode()) || Cools.isEmpty(aiModelRoute.getModelCode())) {
            return R.error("路由编码和模型编码不能为空");
        }
        Date now = new Date();
        aiModelRoute.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3));
        aiModelRoute.setTenantId(getTenantId());
        aiModelRoute.setCreateBy(getLoginUserId());
        aiModelRoute.setCreateTime(now);
        aiModelRoute.setUpdateBy(getLoginUserId());
        aiModelRoute.setUpdateTime(now);
        if (aiModelRoute.getPriority() == null) {
            aiModelRoute.setPriority(1);
        }
        if (aiModelRoute.getStatus() == null) {
            aiModelRoute.setStatus(1);
        }
        if (aiModelRoute.getFailCount() == null) {
            aiModelRoute.setFailCount(0);
        }
        if (aiModelRoute.getSuccessCount() == null) {
            aiModelRoute.setSuccessCount(0);
        }
        if (!aiModelRouteService.save(aiModelRoute)) {
            return R.error("Save Fail");
        }
        return R.ok("Save Success").add(aiModelRoute);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:update')")
    @OperationLog("Update AiRoute")
    @PostMapping("/aiRoute/update")
    public R update(@RequestBody AiModelRoute aiModelRoute) {
        AiModelRoute existed = aiModelRouteService.getTenantRoute(getTenantId(), aiModelRoute.getId());
        if (existed == null) {
            return R.error("route not found");
        }
        aiModelRoute.setTenantId(getTenantId());
        aiModelRoute.setUpdateBy(getLoginUserId());
        aiModelRoute.setUpdateTime(new Date());
        aiModelRoute.setCreateBy(existed.getCreateBy());
        aiModelRoute.setCreateTime(existed.getCreateTime());
        if (!aiModelRouteService.updateById(aiModelRoute)) {
            return R.error("Update Fail");
        }
        return R.ok("Update Success").add(aiModelRoute);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:remove')")
    @OperationLog("Delete AiRoute")
    @PostMapping("/aiRoute/remove/{ids}")
    public R remove(@PathVariable Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        List<AiModelRoute> routes = aiModelRouteService.list(new LambdaQueryWrapper<AiModelRoute>()
                .eq(AiModelRoute::getTenantId, getTenantId())
                .in(AiModelRoute::getId, idList));
        if (routes.size() != idList.size() || !aiModelRouteService.removeByIds(idList)) {
            return R.error("Delete Fail");
        }
        return R.ok("Delete Success").add(ids);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:list')")
    @PostMapping("/aiRoute/export")
    public void export(@RequestBody Map<String, Object> map, HttpServletResponse response) throws Exception {
        ExcelUtil.build(ExcelUtil.create(aiModelRouteService.list(new LambdaQueryWrapper<AiModelRoute>()
                .eq(AiModelRoute::getTenantId, getTenantId())), AiModelRoute.class), response);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:list')")
    @GetMapping("/ai/route/list")
    public R customList(@RequestParam(required = false) String routeCode) {
        LambdaQueryWrapper<AiModelRoute> wrapper = new LambdaQueryWrapper<AiModelRoute>()
                .eq(AiModelRoute::getTenantId, getTenantId())
                .orderByAsc(AiModelRoute::getRouteCode, AiModelRoute::getPriority, AiModelRoute::getId);
        if (!Cools.isEmpty(routeCode)) {
            wrapper.eq(AiModelRoute::getRouteCode, routeCode);
        }
        return R.ok().add(aiModelRouteService.list(wrapper));
    }
    @PreAuthorize("hasAnyAuthority('system:aiRoute:save','system:aiRoute:update')")
    @PostMapping("/ai/route/save")
    public R customSave(@RequestBody AiModelRoute aiModelRoute) {
        if (aiModelRoute.getId() == null) {
            if (!hasAuthority("system:aiRoute:save")) {
                return R.error("无新增权限");
            }
            return save(aiModelRoute);
        }
        if (!hasAuthority("system:aiRoute:update")) {
            return R.error("无更新权限");
        }
        return update(aiModelRoute);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:update')")
    @OperationLog("Toggle AiRoute")
    @PostMapping("/ai/route/toggle")
    public R toggle(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        Integer status = Integer.valueOf(String.valueOf(map.get("status")));
        AiModelRoute route = aiModelRouteService.getTenantRoute(getTenantId(), id);
        if (route == null) {
            return R.error("route not found");
        }
        route.setStatus(status);
        route.setUpdateBy(getLoginUserId());
        route.setUpdateTime(new Date());
        aiModelRouteService.updateById(route);
        return R.ok().add(route);
    }
    @PreAuthorize("hasAuthority('system:aiRoute:update')")
    @OperationLog("Reset AiRoute")
    @PostMapping("/ai/route/reset")
    public R reset(@RequestBody Map<String, Object> map) {
        Long id = Long.valueOf(String.valueOf(map.get("id")));
        if (aiModelRouteService.getTenantRoute(getTenantId(), id) == null) {
            return R.error("route not found");
        }
        aiModelRouteRuntimeService.resetRoute(id);
        return R.ok();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/AuthController.java
@@ -252,3 +252,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/BaseController.java
@@ -5,6 +5,7 @@
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.system.entity.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.List;
@@ -39,6 +40,26 @@
    public Long getTenantId() {
        User loginUser = getLoginUser();
        return loginUser == null ? null : loginUser.getTenantId();
    }
    public boolean hasAuthority(String authority) {
        if (authority == null || authority.trim().isEmpty()) {
            return false;
        }
        try {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication == null || authentication.getAuthorities() == null) {
                return false;
            }
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (grantedAuthority != null && authority.equals(grantedAuthority.getAuthority())) {
                    return true;
                }
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
        return false;
    }
    public <T extends BaseParam> T buildParam(Map<String, Object> map, Class<T> clz) {
@@ -85,3 +106,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/ConfigController.java
@@ -154,3 +154,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/DeptController.java
@@ -151,3 +151,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/DictDataController.java
@@ -119,3 +119,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/DictTypeController.java
@@ -121,3 +121,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FieldsController.java
@@ -118,3 +118,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FieldsItemController.java
@@ -108,3 +108,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowInstanceController.java
@@ -105,3 +105,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepInstanceController.java
@@ -115,3 +115,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepLogController.java
@@ -103,3 +103,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/FlowStepTemplateController.java
@@ -99,3 +99,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/HostController.java
@@ -105,3 +105,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/MatnrRoleMenuController.java
@@ -66,3 +66,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/MenuController.java
@@ -243,3 +243,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/OperationRecordController.java
@@ -85,3 +85,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/PdaRoleMenuController.java
@@ -56,3 +56,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/RoleController.java
@@ -171,3 +171,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SerialRuleController.java
@@ -108,3 +108,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SerialRuleItemController.java
@@ -108,3 +108,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/SubsystemFlowTemplateController.java
@@ -99,3 +99,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskInstanceController.java
@@ -108,3 +108,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskInstanceNodeController.java
@@ -105,3 +105,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateController.java
@@ -108,3 +108,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateMergeController.java
@@ -130,3 +130,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TaskPathTemplateNodeController.java
@@ -108,3 +108,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/TenantController.java
@@ -113,3 +113,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/UserController.java
@@ -214,3 +214,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/UserLoginController.java
@@ -102,3 +102,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/WarehouseRoleMenuController.java
@@ -111,3 +111,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/LoginParam.java
@@ -24,3 +24,4 @@
    private Long tenantId;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/RegisterParam.java
@@ -21,3 +21,4 @@
    private String code;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/RoleScopeParam.java
@@ -24,3 +24,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/TenantInitParam.java
@@ -18,3 +18,4 @@
    private String memo;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/param/UpdatePasswordParam.java
@@ -14,3 +14,4 @@
    private String newPassword;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/result/LoginResult.java
@@ -24,3 +24,4 @@
    private String tenant;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/result/MenuVo.java
@@ -52,3 +52,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/result/SystemInfoVo.java
@@ -16,3 +16,4 @@
    private String mode;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiCallLog.java
New file
@@ -0,0 +1,124 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.system.service.TenantService;
import com.vincent.rsf.server.system.service.UserService;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_call_log")
public class AiCallLog implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "会话ID")
    private String sessionId;
    @ApiModelProperty(value = "诊断记录ID")
    private Long diagnosisId;
    @ApiModelProperty(value = "路由编码")
    private String routeCode;
    @ApiModelProperty(value = "模型编码")
    private String modelCode;
    @ApiModelProperty(value = "尝试序号")
    private Integer attemptNo;
    @ApiModelProperty(value = "请求时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date requestTime;
    @ApiModelProperty(value = "响应时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date responseTime;
    @ApiModelProperty(value = "耗时毫秒")
    private Long spendTime;
    @ApiModelProperty(value = "结果 1:成功 0:失败")
    private Integer result;
    @ApiModelProperty(value = "错误信息")
    private String err;
    @ApiModelProperty(value = "状态 1:正常 0:冻结")
    private Integer status;
    @ApiModelProperty(value = "是否删除 1:是 0:否")
    @TableLogic
    private Integer deleted;
    @ApiModelProperty(value = "租户")
    private Long tenantId;
    @ApiModelProperty(value = "用户")
    private Long userId;
    @ApiModelProperty(value = "创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    public String getResult$() {
        if (this.result == null) {
            return null;
        }
        return Integer.valueOf(1).equals(this.result) ? "成功" : "失败";
    }
    public String getUserId$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.userId);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getTenantId$() {
        TenantService service = SpringUtils.getBean(TenantService.class);
        Tenant tenant = service.getById(this.tenantId);
        if (!Cools.isEmpty(tenant)) {
            return String.valueOf(tenant.getName());
        }
        return null;
    }
    public String getRequestTime$() {
        if (Cools.isEmpty(this.requestTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.requestTime);
    }
    public String getResponseTime$() {
        if (Cools.isEmpty(this.responseTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.responseTime);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiDiagnosisPlan.java
New file
@@ -0,0 +1,144 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_diagnosis_plan")
public class AiDiagnosisPlan implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("编号")
    private String uuid;
    @ApiModelProperty("计划名称")
    private String planName;
    @ApiModelProperty("场景编码")
    private String sceneCode;
    @ApiModelProperty("Cron表达式")
    private String cronExpr;
    @ApiModelProperty("巡检提示词")
    private String prompt;
    @ApiModelProperty("优先模型编码")
    private String preferredModelCode;
    @ApiModelProperty("运行中 1:是 0:否")
    private Integer runningFlag;
    @ApiModelProperty("上次结果 2:运行中 1:成功 0:失败")
    private Integer lastResult;
    @ApiModelProperty("上次诊断记录ID")
    private Long lastDiagnosisId;
    @ApiModelProperty("上次运行时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date lastRunTime;
    @ApiModelProperty("下次运行时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date nextRunTime;
    @ApiModelProperty("最近消息")
    private String lastMessage;
    @ApiModelProperty("状态 1:正常 0:冻结")
    private Integer status;
    @TableLogic
    @ApiModelProperty("是否删除 1:是 0:否")
    private Integer deleted;
    private Long tenantId;
    private Long createBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    private Long updateBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    private String memo;
    public Boolean getStatusBool() {
        if (status == null) {
            return null;
        }
        return Integer.valueOf(1).equals(status);
    }
    public Boolean getRunningFlagBool() {
        if (runningFlag == null) {
            return null;
        }
        return Integer.valueOf(1).equals(runningFlag);
    }
    public String getLastResult$() {
        if (lastResult == null) {
            return "未运行";
        }
        if (Integer.valueOf(2).equals(lastResult)) {
            return "运行中";
        }
        if (Integer.valueOf(1).equals(lastResult)) {
            return "成功";
        }
        if (Integer.valueOf(0).equals(lastResult)) {
            return "失败";
        }
        return String.valueOf(lastResult);
    }
    public String getSceneCode$() {
        if ("system_diagnose".equals(sceneCode)) {
            return "系统诊断";
        }
        if ("general_chat".equals(sceneCode)) {
            return "通用对话";
        }
        return sceneCode;
    }
    public String getLastRunTime$() {
        if (Cools.isEmpty(lastRunTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(lastRunTime);
    }
    public String getNextRunTime$() {
        if (Cools.isEmpty(nextRunTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(nextRunTime);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiDiagnosisRecord.java
New file
@@ -0,0 +1,173 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.system.service.TenantService;
import com.vincent.rsf.server.system.service.UserService;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_diagnosis_record")
public class AiDiagnosisRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "诊断编号")
    private String diagnosisNo;
    @ApiModelProperty(value = "会话ID")
    private String sessionId;
    @ApiModelProperty(value = "场景编码")
    private String sceneCode;
    @ApiModelProperty(value = "问题")
    private String question;
    @ApiModelProperty(value = "结论")
    private String conclusion;
    @ApiModelProperty(value = "报告标题")
    private String reportTitle;
    @ApiModelProperty(value = "执行摘要")
    private String executiveSummary;
    @ApiModelProperty(value = "证据摘要")
    private String evidenceSummary;
    @ApiModelProperty(value = "建议动作")
    private String actionSummary;
    @ApiModelProperty(value = "风险评估")
    private String riskSummary;
    @ApiModelProperty(value = "报告Markdown")
    private String reportMarkdown;
    @ApiModelProperty(value = "工具摘要")
    private String toolSummary;
    @ApiModelProperty(value = "模型编码")
    private String modelCode;
    @ApiModelProperty(value = "结果 2:运行中 1:成功 0:失败")
    private Integer result;
    @ApiModelProperty(value = "错误信息")
    private String err;
    @ApiModelProperty(value = "开始时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date startTime;
    @ApiModelProperty(value = "结束时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date endTime;
    @ApiModelProperty(value = "耗时毫秒")
    private Long spendTime;
    @ApiModelProperty(value = "状态 1:正常 0:冻结")
    private Integer status;
    @ApiModelProperty(value = "是否删除 1:是 0:否")
    @TableLogic
    private Integer deleted;
    @ApiModelProperty(value = "租户")
    private Long tenantId;
    @ApiModelProperty(value = "用户")
    private Long userId;
    @ApiModelProperty(value = "创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    @ApiModelProperty(value = "更新时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    public String getSceneCode$() {
        if (AiSceneCode.SYSTEM_DIAGNOSE.equals(this.sceneCode)) {
            return "系统诊断";
        }
        if (AiSceneCode.GENERAL_CHAT.equals(this.sceneCode)) {
            return "通用对话";
        }
        return this.sceneCode;
    }
    public String getResult$() {
        if (this.result == null) {
            return null;
        }
        if (Integer.valueOf(2).equals(this.result)) {
            return "运行中";
        }
        if (Integer.valueOf(1).equals(this.result)) {
            return "成功";
        }
        if (Integer.valueOf(0).equals(this.result)) {
            return "失败";
        }
        return String.valueOf(this.result);
    }
    public String getUserId$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.userId);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getTenantId$() {
        TenantService service = SpringUtils.getBean(TenantService.class);
        Tenant tenant = service.getById(this.tenantId);
        if (!Cools.isEmpty(tenant)) {
            return String.valueOf(tenant.getName());
        }
        return null;
    }
    public String getStartTime$() {
        if (Cools.isEmpty(this.startTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.startTime);
    }
    public String getEndTime$() {
        if (Cools.isEmpty(this.endTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.endTime);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiDiagnosticToolConfig.java
New file
@@ -0,0 +1,85 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_diagnostic_tool_config")
public class AiDiagnosticToolConfig implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("编号")
    private String uuid;
    @ApiModelProperty("场景编码")
    private String sceneCode;
    @ApiModelProperty("工具编码")
    private String toolCode;
    @ApiModelProperty("工具名称")
    private String toolName;
    @ApiModelProperty("启用 1:是 0:否")
    private Integer enabledFlag;
    @ApiModelProperty("优先级")
    private Integer priority;
    @ApiModelProperty("附加提示词")
    private String toolPrompt;
    @ApiModelProperty("用途范围")
    private String usageScope;
    @ApiModelProperty("状态")
    private Integer status;
    @TableLogic
    private Integer deleted;
    private Long tenantId;
    private Long createBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    private Long updateBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    private String memo;
    public Boolean getEnabledFlagBool() {
        if (enabledFlag == null) {
            return null;
        }
        return Integer.valueOf(1).equals(enabledFlag);
    }
    public Boolean getStatusBool() {
        if (status == null) {
            return null;
        }
        return Integer.valueOf(1).equals(status);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiMcpMount.java
New file
@@ -0,0 +1,118 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_mcp_mount")
public class AiMcpMount implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("编号")
    private String uuid;
    @ApiModelProperty("名称")
    private String name;
    @ApiModelProperty("挂载编码")
    private String mountCode;
    @ApiModelProperty("传输类型")
    private String transportType;
    @ApiModelProperty("地址")
    private String url;
    @ApiModelProperty("认证方式")
    private String authType;
    @ApiModelProperty("认证值")
    private String authValue;
    @ApiModelProperty("用途范围")
    private String usageScope;
    @ApiModelProperty("启用 1:是 0:否")
    private Integer enabledFlag;
    @ApiModelProperty("超时毫秒")
    private Integer timeoutMs;
    @ApiModelProperty("上次测试结果 1:成功 0:失败")
    private Integer lastTestResult;
    @ApiModelProperty("上次测试时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date lastTestTime;
    @ApiModelProperty("上次测试消息")
    private String lastTestMessage;
    @ApiModelProperty("上次工具数")
    private Integer lastToolCount;
    @ApiModelProperty("状态 1:正常 0:冻结")
    private Integer status;
    @TableLogic
    private Integer deleted;
    private Long tenantId;
    private Long createBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    private Long updateBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    private String memo;
    public Boolean getEnabledFlagBool() {
        if (enabledFlag == null) {
            return null;
        }
        return Integer.valueOf(1).equals(enabledFlag);
    }
    public String getLastTestResult$() {
        if (lastTestResult == null) {
            return "未测试";
        }
        return Integer.valueOf(1).equals(lastTestResult) ? "成功" : "失败";
    }
    public String getLastTestTime$() {
        if (Cools.isEmpty(lastTestTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(lastTestTime);
    }
    public Boolean getInternalManaged() {
        return "INTERNAL".equalsIgnoreCase(transportType);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiModelRoute.java
New file
@@ -0,0 +1,140 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.system.service.TenantService;
import com.vincent.rsf.server.system.service.UserService;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_model_route")
public class AiModelRoute implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "编号")
    private String uuid;
    @ApiModelProperty(value = "路由编码")
    private String routeCode;
    @ApiModelProperty(value = "模型编码")
    private String modelCode;
    @ApiModelProperty(value = "优先级")
    private Integer priority;
    @ApiModelProperty(value = "冷却截止时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date cooldownUntil;
    @ApiModelProperty(value = "失败次数")
    private Integer failCount;
    @ApiModelProperty(value = "成功次数")
    private Integer successCount;
    @ApiModelProperty(value = "状态 1:启用 0:停用")
    private Integer status;
    @ApiModelProperty(value = "是否删除 1:是 0:否")
    @TableLogic
    private Integer deleted;
    @ApiModelProperty(value = "租户")
    private Long tenantId;
    @ApiModelProperty(value = "添加人员")
    private Long createBy;
    @ApiModelProperty(value = "添加时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    @ApiModelProperty(value = "修改人员")
    private Long updateBy;
    @ApiModelProperty(value = "修改时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    @ApiModelProperty(value = "备注")
    private String memo;
    public Boolean getStatusBool() {
        if (this.status == null) {
            return null;
        }
        return Integer.valueOf(1).equals(this.status);
    }
    public String getCooldownUntil$() {
        if (Cools.isEmpty(this.cooldownUntil)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.cooldownUntil);
    }
    public String getTenantId$() {
        TenantService service = SpringUtils.getBean(TenantService.class);
        Tenant tenant = service.getById(this.tenantId);
        if (!Cools.isEmpty(tenant)) {
            return String.valueOf(tenant.getName());
        }
        return null;
    }
    public String getCreateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.createBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getCreateTime$() {
        if (Cools.isEmpty(this.createTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public String getUpdateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.updateBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getUpdateTime$() {
        if (Cools.isEmpty(this.updateTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiParam.java
@@ -167,3 +167,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiPromptPublishLog.java
New file
@@ -0,0 +1,50 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_prompt_publish_log")
public class AiPromptPublishLog implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("Prompt模板ID")
    private Long promptTemplateId;
    @ApiModelProperty("场景编码")
    private String sceneCode;
    @ApiModelProperty("模板名称")
    private String templateName;
    @ApiModelProperty("版本号")
    private Integer versionNo;
    @ApiModelProperty("动作")
    private String actionType;
    @ApiModelProperty("动作描述")
    private String actionDesc;
    private Long tenantId;
    private Long createBy;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/AiPromptTemplate.java
New file
@@ -0,0 +1,159 @@
package com.vincent.rsf.server.system.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.SpringUtils;
import com.vincent.rsf.server.ai.constant.AiSceneCode;
import com.vincent.rsf.server.system.service.TenantService;
import com.vincent.rsf.server.system.service.UserService;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@Accessors(chain = true)
@TableName("sys_ai_prompt_template")
public class AiPromptTemplate implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "编号")
    private String uuid;
    @ApiModelProperty(value = "场景编码")
    private String sceneCode;
    @ApiModelProperty(value = "模板名称")
    private String templateName;
    @ApiModelProperty(value = "基础提示词")
    private String basePrompt;
    @ApiModelProperty(value = "工具提示词")
    private String toolPrompt;
    @ApiModelProperty(value = "输出提示词")
    private String outputPrompt;
    @ApiModelProperty(value = "版本号")
    private Integer versionNo;
    @ApiModelProperty(value = "已发布 1:是 0:否")
    private Integer publishedFlag;
    @ApiModelProperty(value = "状态 1:正常 0:冻结")
    private Integer status;
    @ApiModelProperty(value = "是否删除 1:是 0:否")
    @TableLogic
    private Integer deleted;
    @ApiModelProperty(value = "租户")
    private Long tenantId;
    @ApiModelProperty(value = "添加人员")
    private Long createBy;
    @ApiModelProperty(value = "添加时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    @ApiModelProperty(value = "修改人员")
    private Long updateBy;
    @ApiModelProperty(value = "修改时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    @ApiModelProperty(value = "备注")
    private String memo;
    public String getSceneCode$() {
        if (AiSceneCode.SYSTEM_DIAGNOSE.equals(this.sceneCode)) {
            return "系统诊断";
        }
        if (AiSceneCode.GENERAL_CHAT.equals(this.sceneCode)) {
            return "通用对话";
        }
        return this.sceneCode;
    }
    public Boolean getPublishedFlagBool() {
        if (this.publishedFlag == null) {
            return null;
        }
        return Integer.valueOf(1).equals(this.publishedFlag);
    }
    public String getPublishedFlag$() {
        if (this.publishedFlag == null) {
            return null;
        }
        return Integer.valueOf(1).equals(this.publishedFlag) ? "已发布" : "草稿";
    }
    public Boolean getStatusBool() {
        if (this.status == null) {
            return null;
        }
        return Integer.valueOf(1).equals(this.status);
    }
    public String getTenantId$() {
        TenantService service = SpringUtils.getBean(TenantService.class);
        Tenant tenant = service.getById(this.tenantId);
        if (!Cools.isEmpty(tenant)) {
            return String.valueOf(tenant.getName());
        }
        return null;
    }
    public String getCreateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.createBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getCreateTime$() {
        if (Cools.isEmpty(this.createTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public String getUpdateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.updateBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getUpdateTime$() {
        if (Cools.isEmpty(this.updateTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Config.java
@@ -208,3 +208,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Dept.java
@@ -227,3 +227,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/DictData.java
@@ -233,3 +233,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/DictType.java
@@ -195,3 +195,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Fields.java
@@ -231,3 +231,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FieldsItem.java
@@ -213,3 +213,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowInstance.java
@@ -301,3 +301,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowStepInstance.java
@@ -252,3 +252,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowStepLog.java
@@ -109,3 +109,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/FlowStepTemplate.java
@@ -198,3 +198,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Host.java
@@ -117,3 +117,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/MatnrRoleMenu.java
@@ -48,3 +48,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Menu.java
@@ -241,3 +241,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/OperationRecord.java
@@ -153,3 +153,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/PdaRoleMenu.java
@@ -48,3 +48,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Role.java
@@ -134,3 +134,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/RoleMenu.java
@@ -38,3 +38,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/SerialRule.java
@@ -228,3 +228,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/SerialRuleItem.java
@@ -233,3 +233,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/SubsystemFlowTemplate.java
@@ -213,3 +213,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskInstance.java
@@ -390,3 +390,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskInstanceNode.java
@@ -293,3 +293,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskPathTemplate.java
@@ -262,3 +262,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskPathTemplateMerge.java
@@ -246,3 +246,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/TaskPathTemplateNode.java
@@ -203,3 +203,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/Tenant.java
@@ -117,3 +117,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/User.java
@@ -323,3 +323,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/UserLogin.java
@@ -114,3 +114,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/UserRole.java
@@ -38,3 +38,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/entity/WarehouseRoleMenu.java
@@ -48,3 +48,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/CompanyType.java
@@ -25,3 +25,4 @@
        this.desc = desc;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/ConfigType.java
@@ -27,3 +27,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/EmailType.java
@@ -16,3 +16,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/LoginSystemType.java
@@ -11,3 +11,4 @@
    ;
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/SerialRuleReset.java
@@ -27,3 +27,4 @@
        this.desc = desc;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/SerialRuleType.java
@@ -27,3 +27,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/enums/StatusType.java
@@ -12,3 +12,4 @@
        this.val = val;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiCallLogMapper.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiCallLog;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiCallLogMapper extends BaseMapper<AiCallLog> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiDiagnosisPlanMapper.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiDiagnosisPlan;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiDiagnosisPlanMapper extends BaseMapper<AiDiagnosisPlan> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiDiagnosisRecordMapper.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiDiagnosisRecordMapper extends BaseMapper<AiDiagnosisRecord> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiDiagnosticToolConfigMapper.java
New file
@@ -0,0 +1,10 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiDiagnosticToolConfigMapper extends BaseMapper<AiDiagnosticToolConfig> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiMcpMountMapper.java
New file
@@ -0,0 +1,11 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiMcpMountMapper extends BaseMapper<AiMcpMount> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiModelRouteMapper.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiModelRoute;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiModelRouteMapper extends BaseMapper<AiModelRoute> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiParamMapper.java
@@ -10,3 +10,4 @@
public interface AiParamMapper extends BaseMapper<AiParam> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiPromptPublishLogMapper.java
New file
@@ -0,0 +1,10 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiPromptPublishLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AiPromptPublishLogMapper extends BaseMapper<AiPromptPublishLog> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/AiPromptTemplateMapper.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface AiPromptTemplateMapper extends BaseMapper<AiPromptTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/ConfigMapper.java
@@ -11,3 +11,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/DeptMapper.java
@@ -10,3 +10,4 @@
public interface DeptMapper extends BaseMapper<Dept> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/DictDataMapper.java
@@ -10,3 +10,4 @@
public interface DictDataMapper extends BaseMapper<DictData> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/DictTypeMapper.java
@@ -10,3 +10,4 @@
public interface DictTypeMapper extends BaseMapper<DictType> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FieldsItemMapper.java
@@ -10,3 +10,4 @@
public interface FieldsItemMapper extends BaseMapper<FieldsItem> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FieldsMapper.java
@@ -10,3 +10,4 @@
public interface FieldsMapper extends BaseMapper<Fields> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowInstanceMapper.java
@@ -10,3 +10,4 @@
public interface FlowInstanceMapper extends BaseMapper<FlowInstance> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowStepInstanceMapper.java
@@ -10,3 +10,4 @@
public interface FlowStepInstanceMapper extends BaseMapper<FlowStepInstance> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowStepLogMapper.java
@@ -10,3 +10,4 @@
public interface FlowStepLogMapper extends BaseMapper<FlowStepLog> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/FlowStepTemplateMapper.java
@@ -10,3 +10,4 @@
public interface FlowStepTemplateMapper extends BaseMapper<FlowStepTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/HostMapper.java
@@ -10,3 +10,4 @@
public interface HostMapper extends BaseMapper<Host> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/MatnrRoleMenuMapper.java
@@ -13,3 +13,4 @@
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/MenuMapper.java
@@ -11,3 +11,4 @@
public interface MenuMapper extends BaseMapper<Menu> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/OperationRecordMapper.java
@@ -10,3 +10,4 @@
public interface OperationRecordMapper extends BaseMapper<OperationRecord> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/PdaRoleMenuMapper.java
@@ -20,3 +20,4 @@
    List<Long> listStrictlyMenuByRoleId(@Param("roleId") Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/RoleMapper.java
@@ -11,3 +11,4 @@
public interface RoleMapper extends BaseMapper<Role> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/RoleMenuMapper.java
@@ -20,3 +20,4 @@
    List<Long> listStrictlyMenuByRoleId(@Param("roleId") Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/SerialRuleItemMapper.java
@@ -10,3 +10,4 @@
public interface SerialRuleItemMapper extends BaseMapper<SerialRuleItem> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/SerialRuleMapper.java
@@ -10,3 +10,4 @@
public interface SerialRuleMapper extends BaseMapper<SerialRule> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/SubsystemFlowTemplateMapper.java
@@ -10,3 +10,4 @@
public interface SubsystemFlowTemplateMapper extends BaseMapper<SubsystemFlowTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskInstanceMapper.java
@@ -10,3 +10,4 @@
public interface TaskInstanceMapper extends BaseMapper<TaskInstance> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskInstanceNodeMapper.java
@@ -10,3 +10,4 @@
public interface TaskInstanceNodeMapper extends BaseMapper<TaskInstanceNode> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskPathTemplateMapper.java
@@ -10,3 +10,4 @@
public interface TaskPathTemplateMapper extends BaseMapper<TaskPathTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskPathTemplateMergeMapper.java
@@ -10,3 +10,4 @@
public interface TaskPathTemplateMergeMapper extends BaseMapper<TaskPathTemplateMerge> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TaskPathTemplateNodeMapper.java
@@ -10,3 +10,4 @@
public interface TaskPathTemplateNodeMapper extends BaseMapper<TaskPathTemplateNode> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/TenantMapper.java
@@ -11,3 +11,4 @@
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/UserLoginMapper.java
@@ -10,3 +10,4 @@
public interface UserLoginMapper extends BaseMapper<UserLogin> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/UserMapper.java
@@ -24,3 +24,4 @@
    User selectByEmailWithoutTenant(@Param("email") String email, @Param("tenantId") Long tenantId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/UserRoleMapper.java
@@ -16,3 +16,4 @@
    List<Role> selectByUserId(@Param("userId") Long userId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/mapper/WarehouseRoleMenuMapper.java
@@ -14,3 +14,4 @@
    List<Long> listStrictlyMenuByRoleId(@Param("roleId") Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiCallLogService.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiCallLog;
public interface AiCallLogService extends IService<AiCallLog> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiDiagnosisPlanService.java
New file
@@ -0,0 +1,23 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiDiagnosisPlan;
import java.util.Date;
import java.util.List;
public interface AiDiagnosisPlanService extends IService<AiDiagnosisPlan> {
    AiDiagnosisPlan getTenantPlan(Long tenantId, Long id);
    List<AiDiagnosisPlan> listDuePlans(Date now);
    Date calculateNextRunTime(String cronExpr, Date after);
    boolean validateCron(String cronExpr);
    boolean acquireForExecution(Long id, Long operatorId, String lastMessage, Date nextRunTime);
    void finishExecution(Long id, Integer lastResult, Long lastDiagnosisId, String lastMessage, Date finishTime, Date nextRunTime);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiDiagnosisRecordService.java
New file
@@ -0,0 +1,9 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
public interface AiDiagnosisRecordService extends IService<AiDiagnosisRecord> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiDiagnosticToolConfigService.java
New file
@@ -0,0 +1,16 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import java.util.List;
public interface AiDiagnosticToolConfigService extends IService<AiDiagnosticToolConfig> {
    List<AiDiagnosticToolConfig> listTenantConfigs(Long tenantId);
    List<AiDiagnosticToolConfig> listSceneConfigs(Long tenantId, String sceneCode);
    AiDiagnosticToolConfig getTenantConfig(Long tenantId, Long id);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiMcpMountService.java
New file
@@ -0,0 +1,16 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import java.util.List;
public interface AiMcpMountService extends IService<AiMcpMount> {
    List<AiMcpMount> listTenantMounts(Long tenantId);
    AiMcpMount getTenantMount(Long tenantId, Long id);
    AiMcpMount getTenantMountByCode(Long tenantId, String mountCode);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiModelRouteService.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiModelRoute;
import java.util.List;
public interface AiModelRouteService extends IService<AiModelRoute> {
    AiModelRoute getTenantRoute(Long tenantId, Long id);
    List<AiModelRoute> listAvailableRoutes(Long tenantId, String routeCode);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiParamService.java
@@ -17,3 +17,4 @@
    AiParam getDefaultModel();
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiPromptPublishLogService.java
New file
@@ -0,0 +1,15 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiPromptPublishLog;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import java.util.List;
public interface AiPromptPublishLogService extends IService<AiPromptPublishLog> {
    void saveLog(Long tenantId, Long userId, AiPromptTemplate template, String actionType, String actionDesc);
    List<AiPromptPublishLog> listSceneLogs(Long tenantId, String sceneCode);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/AiPromptTemplateService.java
New file
@@ -0,0 +1,24 @@
package com.vincent.rsf.server.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import java.util.List;
public interface AiPromptTemplateService extends IService<AiPromptTemplate> {
    AiPromptTemplate getTenantTemplate(Long tenantId, Long id);
    AiPromptTemplate getPublishedTemplate(Long tenantId, String sceneCode);
    List<AiPromptTemplate> listVersions(Long tenantId, String sceneCode);
    boolean publishTemplate(Long tenantId, Long id, Long userId);
    boolean rollbackTemplate(Long tenantId, Long id, Long userId);
    int nextVersionNo(Long tenantId, String sceneCode);
    AiPromptTemplate copyTemplate(Long tenantId, Long id, Long userId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/ConfigService.java
@@ -12,3 +12,4 @@
    R modiftyStatus(Config config);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/DeptService.java
@@ -7,3 +7,4 @@
public interface DeptService extends IService<Dept> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/DictDataService.java
@@ -6,3 +6,4 @@
public interface DictDataService extends IService<DictData> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/DictTypeService.java
@@ -6,3 +6,4 @@
public interface DictTypeService extends IService<DictType> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FieldsItemService.java
@@ -6,3 +6,4 @@
public interface FieldsItemService extends IService<FieldsItem> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FieldsService.java
@@ -6,3 +6,4 @@
public interface FieldsService extends IService<Fields> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowInstanceService.java
@@ -6,3 +6,4 @@
public interface FlowInstanceService extends IService<FlowInstance> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowStepInstanceService.java
@@ -8,3 +8,4 @@
    boolean jumpCurrent(Long id);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowStepLogService.java
@@ -6,3 +6,4 @@
public interface FlowStepLogService extends IService<FlowStepLog> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/FlowStepTemplateService.java
@@ -6,3 +6,4 @@
public interface FlowStepTemplateService extends IService<FlowStepTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/HostService.java
@@ -7,3 +7,4 @@
public interface HostService extends IService<Host> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/MatnrRoleMenuService.java
@@ -9,3 +9,4 @@
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/MenuService.java
@@ -7,3 +7,4 @@
public interface MenuService extends IService<Menu> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/OperationRecordService.java
@@ -9,3 +9,4 @@
    void saveAsync(OperationRecord operationRecord);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/PdaRoleMenuService.java
@@ -12,3 +12,4 @@
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/RoleMenuService.java
@@ -13,3 +13,4 @@
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/RoleService.java
@@ -7,3 +7,4 @@
public interface RoleService extends IService<Role> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/SerialRuleItemService.java
@@ -6,3 +6,4 @@
public interface SerialRuleItemService extends IService<SerialRuleItem> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/SerialRuleService.java
@@ -6,3 +6,4 @@
public interface SerialRuleService extends IService<SerialRule> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/SubsystemFlowTemplateService.java
@@ -6,3 +6,4 @@
public interface SubsystemFlowTemplateService extends IService<SubsystemFlowTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskInstanceNodeService.java
@@ -6,3 +6,4 @@
public interface TaskInstanceNodeService extends IService<TaskInstanceNode> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskInstanceService.java
@@ -6,3 +6,4 @@
public interface TaskInstanceService extends IService<TaskInstance> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskPathTemplateMergeService.java
@@ -8,3 +8,4 @@
    R createSelectList();
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskPathTemplateNodeService.java
@@ -6,3 +6,4 @@
public interface TaskPathTemplateNodeService extends IService<TaskPathTemplateNode> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TaskPathTemplateService.java
@@ -6,3 +6,4 @@
public interface TaskPathTemplateService extends IService<TaskPathTemplate> {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/TenantService.java
@@ -9,3 +9,4 @@
    Long initTenant(TenantInitParam param);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/UserLoginService.java
@@ -11,3 +11,4 @@
    void saveAsync(Long userId, String token, Integer type, Long tenantId, String memo, HttpServletRequest request);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/UserRoleService.java
@@ -12,3 +12,4 @@
    List<Role> listByUserId(Long userId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/UserService.java
@@ -28,3 +28,4 @@
    User selectByUsernameWithoutTenant(String username, Long tenantId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/WarehouseRoleMenuService.java
@@ -9,3 +9,4 @@
    List<Long> listStrictlyMenuByRoleId(Long roleId);
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiCallLogServiceImpl.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiCallLog;
import com.vincent.rsf.server.system.mapper.AiCallLogMapper;
import com.vincent.rsf.server.system.service.AiCallLogService;
import org.springframework.stereotype.Service;
@Service("aiCallLogService")
public class AiCallLogServiceImpl extends ServiceImpl<AiCallLogMapper, AiCallLog> implements AiCallLogService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiDiagnosisPlanServiceImpl.java
New file
@@ -0,0 +1,90 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiDiagnosisPlan;
import com.vincent.rsf.server.system.mapper.AiDiagnosisPlanMapper;
import com.vincent.rsf.server.system.service.AiDiagnosisPlanService;
import org.springframework.scheduling.support.CronSequenceGenerator;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service("aiDiagnosisPlanService")
public class AiDiagnosisPlanServiceImpl extends ServiceImpl<AiDiagnosisPlanMapper, AiDiagnosisPlan> implements AiDiagnosisPlanService {
    @Override
    public AiDiagnosisPlan getTenantPlan(Long tenantId, Long id) {
        if (tenantId == null || id == null) {
            return null;
        }
        return this.getOne(new LambdaQueryWrapper<AiDiagnosisPlan>()
                .eq(AiDiagnosisPlan::getTenantId, tenantId)
                .eq(AiDiagnosisPlan::getId, id)
                .last("limit 1"));
    }
    @Override
    public List<AiDiagnosisPlan> listDuePlans(Date now) {
        Date target = now == null ? new Date() : now;
        return this.list(new LambdaQueryWrapper<AiDiagnosisPlan>()
                .eq(AiDiagnosisPlan::getStatus, 1)
                .eq(AiDiagnosisPlan::getRunningFlag, 0)
                .isNotNull(AiDiagnosisPlan::getNextRunTime)
                .le(AiDiagnosisPlan::getNextRunTime, target)
                .orderByAsc(AiDiagnosisPlan::getNextRunTime, AiDiagnosisPlan::getId));
    }
    @Override
    public Date calculateNextRunTime(String cronExpr, Date after) {
        if (!validateCron(cronExpr)) {
            return null;
        }
        return new CronSequenceGenerator(cronExpr.trim()).next(after == null ? new Date() : after);
    }
    @Override
    public boolean validateCron(String cronExpr) {
        if (cronExpr == null || cronExpr.trim().isEmpty()) {
            return false;
        }
        return CronSequenceGenerator.isValidExpression(cronExpr.trim());
    }
    @Override
    public boolean acquireForExecution(Long id, Long operatorId, String lastMessage, Date nextRunTime) {
        if (id == null) {
            return false;
        }
        Date now = new Date();
        return this.update(new LambdaUpdateWrapper<AiDiagnosisPlan>()
                .eq(AiDiagnosisPlan::getId, id)
                .eq(AiDiagnosisPlan::getRunningFlag, 0)
                .set(AiDiagnosisPlan::getRunningFlag, 1)
                .set(AiDiagnosisPlan::getLastResult, 2)
                .set(AiDiagnosisPlan::getLastMessage, lastMessage)
                .set(AiDiagnosisPlan::getNextRunTime, nextRunTime)
                .set(AiDiagnosisPlan::getUpdateBy, operatorId)
                .set(AiDiagnosisPlan::getUpdateTime, now));
    }
    @Override
    public void finishExecution(Long id, Integer lastResult, Long lastDiagnosisId, String lastMessage, Date finishTime, Date nextRunTime) {
        if (id == null) {
            return;
        }
        Date now = finishTime == null ? new Date() : finishTime;
        this.update(new LambdaUpdateWrapper<AiDiagnosisPlan>()
                .eq(AiDiagnosisPlan::getId, id)
                .set(AiDiagnosisPlan::getRunningFlag, 0)
                .set(AiDiagnosisPlan::getLastResult, lastResult)
                .set(AiDiagnosisPlan::getLastDiagnosisId, lastDiagnosisId)
                .set(AiDiagnosisPlan::getLastRunTime, now)
                .set(AiDiagnosisPlan::getNextRunTime, nextRunTime)
                .set(AiDiagnosisPlan::getLastMessage, lastMessage)
                .set(AiDiagnosisPlan::getUpdateTime, now));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiDiagnosisRecordServiceImpl.java
New file
@@ -0,0 +1,13 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiDiagnosisRecord;
import com.vincent.rsf.server.system.mapper.AiDiagnosisRecordMapper;
import com.vincent.rsf.server.system.service.AiDiagnosisRecordService;
import org.springframework.stereotype.Service;
@Service("aiDiagnosisRecordService")
public class AiDiagnosisRecordServiceImpl extends ServiceImpl<AiDiagnosisRecordMapper, AiDiagnosisRecord> implements AiDiagnosisRecordService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiDiagnosticToolConfigServiceImpl.java
New file
@@ -0,0 +1,42 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiDiagnosticToolConfig;
import com.vincent.rsf.server.system.mapper.AiDiagnosticToolConfigMapper;
import com.vincent.rsf.server.system.service.AiDiagnosticToolConfigService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("aiDiagnosticToolConfigService")
public class AiDiagnosticToolConfigServiceImpl extends ServiceImpl<AiDiagnosticToolConfigMapper, AiDiagnosticToolConfig> implements AiDiagnosticToolConfigService {
    @Override
    public List<AiDiagnosticToolConfig> listTenantConfigs(Long tenantId) {
        return this.list(new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, tenantId)
                .orderByAsc(AiDiagnosticToolConfig::getSceneCode, AiDiagnosticToolConfig::getPriority, AiDiagnosticToolConfig::getId));
    }
    @Override
    public List<AiDiagnosticToolConfig> listSceneConfigs(Long tenantId, String sceneCode) {
        return this.list(new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, tenantId)
                .eq(AiDiagnosticToolConfig::getSceneCode, sceneCode)
                .eq(AiDiagnosticToolConfig::getStatus, 1)
                .orderByAsc(AiDiagnosticToolConfig::getPriority, AiDiagnosticToolConfig::getId));
    }
    @Override
    public AiDiagnosticToolConfig getTenantConfig(Long tenantId, Long id) {
        if (tenantId == null || id == null) {
            return null;
        }
        return this.getOne(new LambdaQueryWrapper<AiDiagnosticToolConfig>()
                .eq(AiDiagnosticToolConfig::getTenantId, tenantId)
                .eq(AiDiagnosticToolConfig::getId, id)
                .last("limit 1"));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiMcpMountServiceImpl.java
New file
@@ -0,0 +1,44 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiMcpMount;
import com.vincent.rsf.server.system.mapper.AiMcpMountMapper;
import com.vincent.rsf.server.system.service.AiMcpMountService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service("aiMcpMountService")
public class AiMcpMountServiceImpl extends ServiceImpl<AiMcpMountMapper, AiMcpMount> implements AiMcpMountService {
    @Override
    public List<AiMcpMount> listTenantMounts(Long tenantId) {
        return this.list(new LambdaQueryWrapper<AiMcpMount>()
                .eq(AiMcpMount::getTenantId, tenantId)
                .orderByAsc(AiMcpMount::getMountCode, AiMcpMount::getId));
    }
    @Override
    public AiMcpMount getTenantMount(Long tenantId, Long id) {
        if (tenantId == null || id == null) {
            return null;
        }
        return this.getOne(new LambdaQueryWrapper<AiMcpMount>()
                .eq(AiMcpMount::getTenantId, tenantId)
                .eq(AiMcpMount::getId, id)
                .last("limit 1"));
    }
    @Override
    public AiMcpMount getTenantMountByCode(Long tenantId, String mountCode) {
        if (tenantId == null || mountCode == null || mountCode.trim().isEmpty()) {
            return null;
        }
        return this.getOne(new LambdaQueryWrapper<AiMcpMount>()
                .eq(AiMcpMount::getTenantId, tenantId)
                .eq(AiMcpMount::getMountCode, mountCode)
                .last("limit 1"));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiModelRouteServiceImpl.java
New file
@@ -0,0 +1,38 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiModelRoute;
import com.vincent.rsf.server.system.mapper.AiModelRouteMapper;
import com.vincent.rsf.server.system.service.AiModelRouteService;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service("aiModelRouteService")
public class AiModelRouteServiceImpl extends ServiceImpl<AiModelRouteMapper, AiModelRoute> implements AiModelRouteService {
    @Override
    public AiModelRoute getTenantRoute(Long tenantId, Long id) {
        if (tenantId == null || id == null) {
            return null;
        }
        return this.getOne(new LambdaQueryWrapper<AiModelRoute>()
                .eq(AiModelRoute::getTenantId, tenantId)
                .eq(AiModelRoute::getId, id)
                .last("limit 1"));
    }
    @Override
    public List<AiModelRoute> listAvailableRoutes(Long tenantId, String routeCode) {
        Date now = new Date();
        return this.list(new LambdaQueryWrapper<AiModelRoute>()
                .eq(AiModelRoute::getTenantId, tenantId)
                .eq(AiModelRoute::getRouteCode, routeCode)
                .eq(AiModelRoute::getStatus, 1)
                .and(wrapper -> wrapper.isNull(AiModelRoute::getCooldownUntil).or().le(AiModelRoute::getCooldownUntil, now))
                .orderByAsc(AiModelRoute::getPriority, AiModelRoute::getId));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiParamServiceImpl.java
@@ -64,3 +64,4 @@
        return list.isEmpty() ? null : list.get(0);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiPromptPublishLogServiceImpl.java
New file
@@ -0,0 +1,42 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiPromptPublishLog;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import com.vincent.rsf.server.system.mapper.AiPromptPublishLogMapper;
import com.vincent.rsf.server.system.service.AiPromptPublishLogService;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service("aiPromptPublishLogService")
public class AiPromptPublishLogServiceImpl extends ServiceImpl<AiPromptPublishLogMapper, AiPromptPublishLog> implements AiPromptPublishLogService {
    @Override
    public void saveLog(Long tenantId, Long userId, AiPromptTemplate template, String actionType, String actionDesc) {
        if (tenantId == null || template == null) {
            return;
        }
        this.save(new AiPromptPublishLog()
                .setPromptTemplateId(template.getId())
                .setSceneCode(template.getSceneCode())
                .setTemplateName(template.getTemplateName())
                .setVersionNo(template.getVersionNo())
                .setActionType(actionType)
                .setActionDesc(actionDesc)
                .setTenantId(tenantId)
                .setCreateBy(userId)
                .setCreateTime(new Date()));
    }
    @Override
    public List<AiPromptPublishLog> listSceneLogs(Long tenantId, String sceneCode) {
        return this.list(new LambdaQueryWrapper<AiPromptPublishLog>()
                .eq(AiPromptPublishLog::getTenantId, tenantId)
                .eq(sceneCode != null && !sceneCode.trim().isEmpty(), AiPromptPublishLog::getSceneCode, sceneCode)
                .orderByDesc(AiPromptPublishLog::getCreateTime, AiPromptPublishLog::getId));
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/AiPromptTemplateServiceImpl.java
New file
@@ -0,0 +1,131 @@
package com.vincent.rsf.server.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.entity.AiPromptTemplate;
import com.vincent.rsf.server.system.mapper.AiPromptTemplateMapper;
import com.vincent.rsf.server.system.service.AiPromptPublishLogService;
import com.vincent.rsf.server.system.service.AiPromptTemplateService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
@Service("aiPromptTemplateService")
public class AiPromptTemplateServiceImpl extends ServiceImpl<AiPromptTemplateMapper, AiPromptTemplate> implements AiPromptTemplateService {
    @Resource
    private AiPromptPublishLogService aiPromptPublishLogService;
    @Override
    public AiPromptTemplate getTenantTemplate(Long tenantId, Long id) {
        if (tenantId == null || id == null) {
            return null;
        }
        return this.getOne(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, tenantId)
                .eq(AiPromptTemplate::getId, id)
                .last("limit 1"));
    }
    @Override
    public AiPromptTemplate getPublishedTemplate(Long tenantId, String sceneCode) {
        return this.getOne(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, tenantId)
                .eq(AiPromptTemplate::getSceneCode, sceneCode)
                .eq(AiPromptTemplate::getPublishedFlag, 1)
                .eq(AiPromptTemplate::getStatus, 1)
                .orderByDesc(AiPromptTemplate::getVersionNo, AiPromptTemplate::getId)
                .last("limit 1"));
    }
    @Override
    public List<AiPromptTemplate> listVersions(Long tenantId, String sceneCode) {
        return this.list(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, tenantId)
                .eq(AiPromptTemplate::getSceneCode, sceneCode)
                .orderByDesc(AiPromptTemplate::getVersionNo, AiPromptTemplate::getId));
    }
    @Override
    public boolean publishTemplate(Long tenantId, Long id, Long userId) {
        AiPromptTemplate target = getTenantTemplate(tenantId, id);
        if (target == null) {
            return false;
        }
        boolean updated = switchPublishedTemplate(tenantId, userId, target);
        if (updated) {
            aiPromptPublishLogService.saveLog(tenantId, userId, target, "publish", "发布 Prompt 版本");
        }
        return updated;
    }
    @Override
    public boolean rollbackTemplate(Long tenantId, Long id, Long userId) {
        AiPromptTemplate target = getTenantTemplate(tenantId, id);
        if (target == null) {
            return false;
        }
        boolean updated = switchPublishedTemplate(tenantId, userId, target);
        if (updated) {
            aiPromptPublishLogService.saveLog(tenantId, userId, target, "rollback", "回滚到指定 Prompt 版本");
        }
        return updated;
    }
    private boolean switchPublishedTemplate(Long tenantId, Long userId, AiPromptTemplate target) {
        this.update(new LambdaUpdateWrapper<AiPromptTemplate>()
                .set(AiPromptTemplate::getPublishedFlag, 0)
                .eq(AiPromptTemplate::getTenantId, tenantId)
                .eq(AiPromptTemplate::getSceneCode, target.getSceneCode())
                .eq(AiPromptTemplate::getPublishedFlag, 1));
        target.setPublishedFlag(1);
        target.setUpdateBy(userId);
        target.setUpdateTime(new Date());
        return this.updateById(target);
    }
    @Override
    public int nextVersionNo(Long tenantId, String sceneCode) {
        AiPromptTemplate latest = this.getOne(new LambdaQueryWrapper<AiPromptTemplate>()
                .eq(AiPromptTemplate::getTenantId, tenantId)
                .eq(AiPromptTemplate::getSceneCode, sceneCode)
                .orderByDesc(AiPromptTemplate::getVersionNo, AiPromptTemplate::getId)
                .last("limit 1"));
        return latest == null || latest.getVersionNo() == null ? 1 : latest.getVersionNo() + 1;
    }
    @Override
    public AiPromptTemplate copyTemplate(Long tenantId, Long id, Long userId) {
        AiPromptTemplate source = getTenantTemplate(tenantId, id);
        if (source == null) {
            return null;
        }
        Date now = new Date();
        AiPromptTemplate copied = new AiPromptTemplate()
                .setUuid(String.valueOf(System.currentTimeMillis()))
                .setSceneCode(source.getSceneCode())
                .setTemplateName((source.getTemplateName() == null ? source.getSceneCode() : source.getTemplateName()) + "-副本")
                .setBasePrompt(source.getBasePrompt())
                .setToolPrompt(source.getToolPrompt())
                .setOutputPrompt(source.getOutputPrompt())
                .setVersionNo(nextVersionNo(tenantId, source.getSceneCode()))
                .setPublishedFlag(0)
                .setStatus(source.getStatus())
                .setDeleted(0)
                .setTenantId(tenantId)
                .setCreateBy(userId)
                .setCreateTime(now)
                .setUpdateBy(userId)
                .setUpdateTime(now)
                .setMemo(source.getMemo());
        if (!this.save(copied)) {
            return null;
        }
        aiPromptPublishLogService.saveLog(tenantId, userId, copied, "copy", "复制 Prompt 版本为新草稿");
        return copied;
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/ConfigServiceImpl.java
@@ -138,3 +138,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/DeptServiceImpl.java
@@ -10,3 +10,4 @@
public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements DeptService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/DictDataServiceImpl.java
@@ -10,3 +10,4 @@
public class DictDataServiceImpl extends ServiceImpl<DictDataMapper, DictData> implements DictDataService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/DictTypeServiceImpl.java
@@ -10,3 +10,4 @@
public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictType> implements DictTypeService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FieldsItemServiceImpl.java
@@ -10,3 +10,4 @@
public class FieldsItemServiceImpl extends ServiceImpl<FieldsItemMapper, FieldsItem> implements FieldsItemService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FieldsServiceImpl.java
@@ -15,3 +15,4 @@
public class FieldsServiceImpl extends ServiceImpl<FieldsMapper, Fields> implements FieldsService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowInstanceServiceImpl.java
@@ -10,3 +10,4 @@
public class FlowInstanceServiceImpl extends ServiceImpl<FlowInstanceMapper, FlowInstance> implements FlowInstanceService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowStepInstanceServiceImpl.java
@@ -89,3 +89,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowStepLogServiceImpl.java
@@ -10,3 +10,4 @@
public class FlowStepLogServiceImpl extends ServiceImpl<FlowStepLogMapper, FlowStepLog> implements FlowStepLogService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/FlowStepTemplateServiceImpl.java
@@ -10,3 +10,4 @@
public class FlowStepTemplateServiceImpl extends ServiceImpl<FlowStepTemplateMapper, FlowStepTemplate> implements FlowStepTemplateService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/HostServiceImpl.java
@@ -10,3 +10,4 @@
public class HostServiceImpl extends ServiceImpl<HostMapper, Host> implements HostService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/MatnrRoleMenuServiceImpl.java
@@ -15,3 +15,4 @@
        return baseMapper.listStrictlyMenuByRoleId(roleId);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/MenuServiceImpl.java
@@ -10,3 +10,4 @@
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/OperationRecordServiceImpl.java
@@ -17,3 +17,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/PdaRoleMenuServiceImpl.java
@@ -22,3 +22,4 @@
        return baseMapper.listStrictlyMenuByRoleId(roleId);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/RoleMenuServiceImpl.java
@@ -23,3 +23,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/RoleServiceImpl.java
@@ -11,3 +11,4 @@
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/SerialRuleItemServiceImpl.java
@@ -10,3 +10,4 @@
public class SerialRuleItemServiceImpl extends ServiceImpl<SerialRuleItemMapper, SerialRuleItem> implements SerialRuleItemService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/SerialRuleServiceImpl.java
@@ -10,3 +10,4 @@
public class SerialRuleServiceImpl extends ServiceImpl<SerialRuleMapper, SerialRule> implements SerialRuleService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/SubsystemFlowTemplateServiceImpl.java
@@ -10,3 +10,4 @@
public class SubsystemFlowTemplateServiceImpl extends ServiceImpl<SubsystemFlowTemplateMapper, SubsystemFlowTemplate> implements SubsystemFlowTemplateService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskInstanceNodeServiceImpl.java
@@ -10,3 +10,4 @@
public class TaskInstanceNodeServiceImpl extends ServiceImpl<TaskInstanceNodeMapper, TaskInstanceNode> implements TaskInstanceNodeService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskInstanceServiceImpl.java
@@ -10,3 +10,4 @@
public class TaskInstanceServiceImpl extends ServiceImpl<TaskInstanceMapper, TaskInstance> implements TaskInstanceService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskPathTemplateMergeServiceImpl.java
@@ -51,3 +51,4 @@
        return R.ok(maps);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskPathTemplateNodeServiceImpl.java
@@ -10,3 +10,4 @@
public class TaskPathTemplateNodeServiceImpl extends ServiceImpl<TaskPathTemplateNodeMapper, TaskPathTemplateNode> implements TaskPathTemplateNodeService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TaskPathTemplateServiceImpl.java
@@ -10,3 +10,4 @@
public class TaskPathTemplateServiceImpl extends ServiceImpl<TaskPathTemplateMapper, TaskPathTemplate> implements TaskPathTemplateService {
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/TenantServiceImpl.java
@@ -137,3 +137,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/UserLoginServiceImpl.java
@@ -30,3 +30,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/UserRoleServiceImpl.java
@@ -18,3 +18,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/UserServiceImpl.java
@@ -83,3 +83,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/service/impl/WarehouseRoleMenuServiceImpl.java
@@ -16,3 +16,4 @@
        return this.baseMapper.listStrictlyMenuByRoleId(roleId);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/utils/ExtendFieldsUtils.java
@@ -61,3 +61,4 @@
//    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/utils/SerialRuleUtils.java
@@ -148,3 +148,4 @@
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/utils/SystemAuthUtils.java
@@ -73,3 +73,4 @@
    }
}
rsf-server/src/main/resources/application.yml
@@ -47,16 +47,17 @@
ai:
  session-ttl-seconds: 86400
  max-context-messages: 12
  default-model-code: mock-general
  default-model-code: deepseek-ai/DeepSeek-V3.2
  system-prompt: 你是WMS系统内的智能助手,回答时优先保持准确、简洁,并结合上下文帮助用户理解仓储业务。
  diagnosis-system-prompt: 你是一名资深WMS智能诊断助手,目标是结合当前系统上下文对仓库运行情况做巡检分析。回答时禁止凭空猜测,必须优先依据提供的实时摘要进行判断。请按“问题概述、关键证据、可能原因、建议动作、风险评估”的结构输出,并优先给出可执行建议。
  route-fail-threshold: 3
  route-cooldown-minutes: 10
  diagnostic-log-window-hours: 24
  api-failure-window-hours: 24
  models:
    - code: mock-general
      name: Mock General
      provider: mock
      enabled: true
    - code: mock-creative
      name: Mock Creative
      provider: mock
    - code: deepseek-ai/DeepSeek-V3.2
      name: DEEPSEEK
      provider: openai
      enabled: true
# 下位机配置
version/db/20260311_ai_param.sql
File was deleted
version/db/20260311_ai_param_menu.sql
File was deleted
version/db/20260317_ai_all_in_one.sql
New file
@@ -0,0 +1,1194 @@
-- AI 全量迁移聚合脚本
-- 该文件聚合了原有全部 AI 相关迁移 SQL。
-- 若已执行过部分脚本,可重复执行;各段脚本均保持幂等或兼容更新逻辑。
-- >>> 20260311_ai_param.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `sys_ai_param` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `uuid` varchar(255) DEFAULT NULL COMMENT '编号',
  `name` varchar(255) DEFAULT NULL COMMENT '名称',
  `model_code` varchar(255) DEFAULT NULL COMMENT '模型编码',
  `provider` varchar(255) DEFAULT NULL COMMENT '供应商',
  `chat_url` varchar(512) DEFAULT NULL COMMENT '聊天地址',
  `api_key` varchar(512) DEFAULT NULL COMMENT 'API密钥',
  `model_name` varchar(255) DEFAULT NULL COMMENT '模型名称',
  `system_prompt` text COMMENT '系统提示词',
  `max_context_messages` int(11) DEFAULT NULL COMMENT '上下文轮数',
  `default_flag` int(1) NOT NULL DEFAULT '0' COMMENT '默认模型{1:是,0:否}',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户[sys_tenant]',
  `create_by` bigint(20) DEFAULT NULL COMMENT '添加人员[sys_user]',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '修改人员[sys_user]',
  `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_ai_param_model_code` (`model_code`),
  KEY `idx_ai_param_deleted_code` (`deleted`,`model_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `sys_ai_param`
(`uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514305', '通用助手', 'mock-general', 'mock', NULL, NULL, 'mock-general', '你是WMS系统内的智能助手,回答时优先保持准确、简洁,并结合上下文帮助用户理解仓储业务。', 12, 1, 1, 1, 0, 1, 2, NOW(), 2, NOW(), '默认演示模型'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1
    FROM `sys_ai_param`
    WHERE `model_code` = 'mock-general'
);
INSERT INTO `sys_ai_param`
(`uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514306', '创意助手', 'mock-creative', 'mock', NULL, NULL, 'mock-creative', '你是WMS系统内的智能助手,回答时可以更灵活地组织表达,但结论必须准确。', 12, 0, 2, 1, 0, 1, 2, NOW(), 2, NOW(), '演示创意模型'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1
    FROM `sys_ai_param`
    WHERE `model_code` = 'mock-creative'
);
SET @ai_parent_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `component` = 'aiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiParam', 1, 'menu.system', '1', 'menu.system', '/system/aiParam', 'aiParam', NULL, NULL, 0, NULL, 'SmartToy', 9, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_parent_menu_id IS NULL;
SET @ai_parent_menu_id := COALESCE(
    @ai_parent_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `component` = 'aiParam'
        LIMIT 1
    )
);
SET @ai_query_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Query AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_query_menu_id IS NULL;
SET @ai_query_menu_id := COALESCE(
    @ai_query_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Query AiParam'
        LIMIT 1
    )
);
SET @ai_create_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Create AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_create_menu_id IS NULL;
SET @ai_create_menu_id := COALESCE(
    @ai_create_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Create AiParam'
        LIMIT 1
    )
);
SET @ai_update_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Update AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_update_menu_id IS NULL;
SET @ai_update_menu_id := COALESCE(
    @ai_update_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Update AiParam'
        LIMIT 1
    )
);
SET @ai_delete_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Delete AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_delete_menu_id IS NULL;
SET @ai_delete_menu_id := COALESCE(
    @ai_delete_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Delete AiParam'
        LIMIT 1
    )
);
SET @ai_export_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Export AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Export AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 4, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_export_menu_id IS NULL;
SET @ai_export_menu_id := COALESCE(
    @ai_export_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Export AiParam'
        LIMIT 1
    )
);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_parent_menu_id
FROM DUAL
WHERE @ai_parent_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_parent_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_query_menu_id
FROM DUAL
WHERE @ai_query_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_query_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_create_menu_id
FROM DUAL
WHERE @ai_create_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_create_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_update_menu_id
FROM DUAL
WHERE @ai_update_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_update_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_delete_menu_id
FROM DUAL
WHERE @ai_delete_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_delete_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_export_menu_id
FROM DUAL
WHERE @ai_export_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_export_menu_id
  );
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260311_ai_param_menu.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET @ai_parent_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `component` = 'aiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiParam', 1, 'menu.system', '1', 'menu.system', '/system/aiParam', 'aiParam', NULL, NULL, 0, NULL, 'SmartToy', 9, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_parent_menu_id IS NULL;
SET @ai_parent_menu_id := COALESCE(
    @ai_parent_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `component` = 'aiParam'
        LIMIT 1
    )
);
SET @ai_query_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Query AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_query_menu_id IS NULL;
SET @ai_query_menu_id := COALESCE(
    @ai_query_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Query AiParam'
        LIMIT 1
    )
);
SET @ai_create_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Create AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_create_menu_id IS NULL;
SET @ai_create_menu_id := COALESCE(
    @ai_create_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Create AiParam'
        LIMIT 1
    )
);
SET @ai_update_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Update AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_update_menu_id IS NULL;
SET @ai_update_menu_id := COALESCE(
    @ai_update_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Update AiParam'
        LIMIT 1
    )
);
SET @ai_delete_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Delete AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_delete_menu_id IS NULL;
SET @ai_delete_menu_id := COALESCE(
    @ai_delete_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Delete AiParam'
        LIMIT 1
    )
);
SET @ai_export_menu_id := (
    SELECT `id`
    FROM `sys_menu`
    WHERE `parent_id` = @ai_parent_menu_id
      AND `name` = 'Export AiParam'
    LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Export AiParam', @ai_parent_menu_id, NULL, CONCAT('1,', @ai_parent_menu_id), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiParam:list', NULL, 4, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL
WHERE @ai_export_menu_id IS NULL;
SET @ai_export_menu_id := COALESCE(
    @ai_export_menu_id,
    LAST_INSERT_ID(),
    (
        SELECT `id`
        FROM `sys_menu`
        WHERE `parent_id` = @ai_parent_menu_id
          AND `name` = 'Export AiParam'
        LIMIT 1
    )
);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_parent_menu_id
FROM DUAL
WHERE @ai_parent_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_parent_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_query_menu_id
FROM DUAL
WHERE @ai_query_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_query_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_create_menu_id
FROM DUAL
WHERE @ai_create_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_create_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_update_menu_id
FROM DUAL
WHERE @ai_update_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_update_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_delete_menu_id
FROM DUAL
WHERE @ai_delete_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_delete_menu_id
  );
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, @ai_export_menu_id
FROM DUAL
WHERE @ai_export_menu_id IS NOT NULL
  AND NOT EXISTS (
      SELECT 1
      FROM `sys_role_menu`
      WHERE `role_id` = 1
        AND `menu_id` = @ai_export_menu_id
  );
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_chat_storage.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `sys_ai_chat_session` (
  `id` varchar(64) NOT NULL COMMENT '会话ID',
  `title` varchar(255) DEFAULT NULL COMMENT '会话标题',
  `model_code` varchar(128) DEFAULT NULL COMMENT '模型编码',
  `last_message` varchar(255) DEFAULT NULL COMMENT '最近消息摘要',
  `last_message_at` datetime DEFAULT NULL COMMENT '最近消息时间',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_ai_chat_session_owner` (`tenant_id`,`user_id`,`deleted`),
  KEY `idx_ai_chat_session_update` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI会话表';
CREATE TABLE IF NOT EXISTS `sys_ai_chat_message` (
  `id` varchar(64) NOT NULL COMMENT '消息ID',
  `session_id` varchar(64) NOT NULL COMMENT '会话ID',
  `role` varchar(32) DEFAULT NULL COMMENT '角色',
  `content` longtext COMMENT '消息内容',
  `model_code` varchar(128) DEFAULT NULL COMMENT '模型编码',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_ai_chat_message_session` (`session_id`,`deleted`,`create_time`),
  KEY `idx_ai_chat_message_owner` (`tenant_id`,`user_id`,`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI消息表';
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_phase2.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `sys_ai_prompt_template` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `uuid` varchar(64) DEFAULT NULL COMMENT '编号',
  `scene_code` varchar(64) NOT NULL COMMENT '场景编码',
  `template_name` varchar(255) DEFAULT NULL COMMENT '模板名称',
  `base_prompt` longtext COMMENT '基础提示词',
  `tool_prompt` longtext COMMENT '工具提示词',
  `output_prompt` longtext COMMENT '输出提示词',
  `version_no` int(11) DEFAULT NULL COMMENT '版本号',
  `published_flag` int(1) NOT NULL DEFAULT '0' COMMENT '已发布{1:是,0:否}',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_ai_prompt_scene` (`tenant_id`,`scene_code`,`published_flag`,`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI提示词模板';
CREATE TABLE IF NOT EXISTS `sys_ai_diagnosis_record` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `diagnosis_no` varchar(64) DEFAULT NULL COMMENT '诊断编号',
  `session_id` varchar(64) DEFAULT NULL COMMENT '会话ID',
  `scene_code` varchar(64) DEFAULT NULL COMMENT '场景编码',
  `question` longtext COMMENT '问题',
  `conclusion` longtext COMMENT '结论',
  `tool_summary` longtext COMMENT '工具摘要',
  `model_code` varchar(128) DEFAULT NULL COMMENT '模型编码',
  `result` int(1) NOT NULL DEFAULT '2' COMMENT '结果{2:运行中,1:成功,0:失败}',
  `err` varchar(1000) DEFAULT NULL COMMENT '错误信息',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `spend_time` bigint(20) DEFAULT NULL COMMENT '耗时毫秒',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_ai_diag_owner` (`tenant_id`,`scene_code`,`deleted`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI诊断记录';
CREATE TABLE IF NOT EXISTS `sys_ai_call_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `session_id` varchar(64) DEFAULT NULL COMMENT '会话ID',
  `diagnosis_id` bigint(20) DEFAULT NULL COMMENT '诊断记录ID',
  `route_code` varchar(64) DEFAULT NULL COMMENT '路由编码',
  `model_code` varchar(128) DEFAULT NULL COMMENT '模型编码',
  `attempt_no` int(11) DEFAULT NULL COMMENT '尝试序号',
  `request_time` datetime DEFAULT NULL COMMENT '请求时间',
  `response_time` datetime DEFAULT NULL COMMENT '响应时间',
  `spend_time` bigint(20) DEFAULT NULL COMMENT '耗时毫秒',
  `result` int(1) NOT NULL DEFAULT '0' COMMENT '结果{1:成功,0:失败}',
  `err` varchar(1000) DEFAULT NULL COMMENT '错误信息',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_ai_call_owner` (`tenant_id`,`route_code`,`deleted`,`create_time`),
  KEY `idx_ai_call_diagnosis` (`diagnosis_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI调用日志';
CREATE TABLE IF NOT EXISTS `sys_ai_model_route` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `uuid` varchar(64) DEFAULT NULL COMMENT '编号',
  `route_code` varchar(64) NOT NULL COMMENT '路由编码',
  `model_code` varchar(128) NOT NULL COMMENT '模型编码',
  `priority` int(11) NOT NULL DEFAULT '1' COMMENT '优先级',
  `cooldown_until` datetime DEFAULT NULL COMMENT '冷却截止时间',
  `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败次数',
  `success_count` int(11) NOT NULL DEFAULT '0' COMMENT '成功次数',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:启用,0:停用}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_ai_route_code` (`tenant_id`,`route_code`,`status`,`deleted`,`priority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI模型路由';
INSERT INTO `sys_ai_prompt_template`
(`uuid`, `scene_code`, `template_name`, `base_prompt`, `tool_prompt`, `output_prompt`, `version_no`, `published_flag`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514310', 'general_chat', '通用对话默认模板', '你是WMS系统内的智能助手,回答时优先保持准确、简洁,并结合上下文帮助用户理解仓储业务。', '如存在可用上下文,请优先引用实时数据回答。', '回答请直接给出结论,必要时补充说明。', 1, 1, 1, 0, 1, 2, NOW(), 2, NOW(), '默认模板'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM `sys_ai_prompt_template` WHERE `tenant_id` = 1 AND `scene_code` = 'general_chat' AND `version_no` = 1
);
INSERT INTO `sys_ai_prompt_template`
(`uuid`, `scene_code`, `template_name`, `base_prompt`, `tool_prompt`, `output_prompt`, `version_no`, `published_flag`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514311', 'system_diagnose', '系统诊断默认模板', '你是一名资深WMS智能诊断助手,目标是结合当前系统上下文对仓库运行情况做巡检分析。', '请先汇总库存、任务、设备站点、异常日志与AI调用失败信息,再判断是否存在异常。', '请按“问题概述、关键证据、可能原因、建议动作、风险评估”的结构输出。', 1, 1, 1, 0, 1, 2, NOW(), 2, NOW(), '默认模板'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM `sys_ai_prompt_template` WHERE `tenant_id` = 1 AND `scene_code` = 'system_diagnose' AND `version_no` = 1
);
INSERT INTO `sys_ai_model_route`
(`uuid`, `route_code`, `model_code`, `priority`, `cooldown_until`, `fail_count`, `success_count`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514312', 'general_chat', 'deepseek-ai/DeepSeek-V3.2', 1, NULL, 0, 0, 1, 0, 1, 2, NOW(), 2, NOW(), '默认通用对话模型'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM `sys_ai_model_route` WHERE `tenant_id` = 1 AND `route_code` = 'general_chat' AND `model_code` = 'deepseek-ai/DeepSeek-V3.2'
);
INSERT INTO `sys_ai_model_route`
(`uuid`, `route_code`, `model_code`, `priority`, `cooldown_until`, `fail_count`, `success_count`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514314', 'system_diagnose', 'deepseek-ai/DeepSeek-V3.2', 1, NULL, 0, 0, 1, 0, 1, 2, NOW(), 2, NOW(), '默认诊断模型'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM `sys_ai_model_route` WHERE `tenant_id` = 1 AND `route_code` = 'system_diagnose' AND `model_code` = 'deepseek-ai/DeepSeek-V3.2'
);
SET @system_menu_ai_prompt := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiPrompt' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiPrompt', 1, 'menu.system', '1', 'menu.system', '/system/aiPrompt', 'aiPrompt', NULL, NULL, 0, NULL, 'Psychology', 10, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_prompt IS NULL;
SET @system_menu_ai_prompt := COALESCE(@system_menu_ai_prompt, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiPrompt' LIMIT 1));
SET @system_menu_ai_diagnosis := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiDiagnosis' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiDiagnosis', 1, 'menu.system', '1', 'menu.system', '/system/aiDiagnosis', 'aiDiagnosis', NULL, NULL, 0, NULL, 'FactCheck', 11, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_diagnosis IS NULL;
SET @system_menu_ai_diagnosis := COALESCE(@system_menu_ai_diagnosis, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiDiagnosis' LIMIT 1));
SET @system_menu_ai_call_log := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiCallLog' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiCallLog', 1, 'menu.system', '1', 'menu.system', '/system/aiCallLog', 'aiCallLog', NULL, NULL, 0, NULL, 'History', 12, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_call_log IS NULL;
SET @system_menu_ai_call_log := COALESCE(@system_menu_ai_call_log, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiCallLog' LIMIT 1));
SET @system_menu_ai_route := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiRoute' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiRoute', 1, 'menu.system', '1', 'menu.system', '/system/aiRoute', 'aiRoute', NULL, NULL, 0, NULL, 'Route', 13, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_route IS NULL;
SET @system_menu_ai_route := COALESCE(@system_menu_ai_route, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiRoute' LIMIT 1));
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiPrompt', @system_menu_ai_prompt, NULL, CONCAT('1,', @system_menu_ai_prompt), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiPrompt:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_prompt AND `authority` = 'system:aiPrompt:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiPrompt', @system_menu_ai_prompt, NULL, CONCAT('1,', @system_menu_ai_prompt), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiPrompt:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_prompt AND `authority` = 'system:aiPrompt:save');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiPrompt', @system_menu_ai_prompt, NULL, CONCAT('1,', @system_menu_ai_prompt), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiPrompt:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_prompt AND `authority` = 'system:aiPrompt:update');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiPrompt', @system_menu_ai_prompt, NULL, CONCAT('1,', @system_menu_ai_prompt), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiPrompt:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_prompt AND `authority` = 'system:aiPrompt:remove');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Publish AiPrompt', @system_menu_ai_prompt, NULL, CONCAT('1,', @system_menu_ai_prompt), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiPrompt:publish', NULL, 4, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_prompt AND `authority` = 'system:aiPrompt:publish');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiDiagnosis', @system_menu_ai_diagnosis, NULL, CONCAT('1,', @system_menu_ai_diagnosis), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiDiagnosis:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_diagnosis AND `authority` = 'system:aiDiagnosis:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiCallLog', @system_menu_ai_call_log, NULL, CONCAT('1,', @system_menu_ai_call_log), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiCallLog:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_call_log AND `authority` = 'system:aiCallLog:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiRoute', @system_menu_ai_route, NULL, CONCAT('1,', @system_menu_ai_route), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiRoute:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_route AND `authority` = 'system:aiRoute:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiRoute', @system_menu_ai_route, NULL, CONCAT('1,', @system_menu_ai_route), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiRoute:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_route AND `authority` = 'system:aiRoute:save');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiRoute', @system_menu_ai_route, NULL, CONCAT('1,', @system_menu_ai_route), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiRoute:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_route AND `authority` = 'system:aiRoute:update');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiRoute', @system_menu_ai_route, NULL, CONCAT('1,', @system_menu_ai_route), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiRoute:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_route AND `authority` = 'system:aiRoute:remove');
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `authority` IN (
    'system:aiPrompt:list', 'system:aiPrompt:save', 'system:aiPrompt:update', 'system:aiPrompt:remove', 'system:aiPrompt:publish',
    'system:aiDiagnosis:list', 'system:aiCallLog:list',
    'system:aiRoute:list', 'system:aiRoute:save', 'system:aiRoute:update', 'system:aiRoute:remove'
)
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `component` IN ('aiPrompt', 'aiDiagnosis', 'aiCallLog', 'aiRoute')
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_phase3.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `sys_ai_diagnostic_tool_config` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `uuid` varchar(64) DEFAULT NULL COMMENT '编号',
  `scene_code` varchar(64) NOT NULL COMMENT '场景编码',
  `tool_code` varchar(64) NOT NULL COMMENT '工具编码',
  `tool_name` varchar(128) DEFAULT NULL COMMENT '工具名称',
  `enabled_flag` int(1) NOT NULL DEFAULT '1' COMMENT '启用{1:是,0:否}',
  `priority` int(11) NOT NULL DEFAULT '1' COMMENT '优先级',
  `tool_prompt` longtext COMMENT '工具提示词',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_ai_tool_scene` (`tenant_id`,`scene_code`,`status`,`deleted`,`priority`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI诊断工具配置';
CREATE TABLE IF NOT EXISTS `sys_ai_prompt_publish_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `prompt_template_id` bigint(20) DEFAULT NULL COMMENT 'Prompt模板ID',
  `scene_code` varchar(64) DEFAULT NULL COMMENT '场景编码',
  `template_name` varchar(255) DEFAULT NULL COMMENT '模板名称',
  `version_no` int(11) DEFAULT NULL COMMENT '版本号',
  `action_type` varchar(64) DEFAULT NULL COMMENT '动作类型',
  `action_desc` varchar(255) DEFAULT NULL COMMENT '动作描述',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_ai_prompt_publish_log` (`tenant_id`,`scene_code`,`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI提示词发布日志';
SET @sql := IF(
  EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_ai_diagnosis_record' AND COLUMN_NAME = 'report_title'),
  'SELECT 1',
  'ALTER TABLE `sys_ai_diagnosis_record` ADD COLUMN `report_title` varchar(255) DEFAULT NULL COMMENT ''报告标题'' AFTER `conclusion`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql := IF(
  EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_ai_diagnosis_record' AND COLUMN_NAME = 'executive_summary'),
  'SELECT 1',
  'ALTER TABLE `sys_ai_diagnosis_record` ADD COLUMN `executive_summary` longtext COMMENT ''执行摘要'' AFTER `report_title`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql := IF(
  EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_ai_diagnosis_record' AND COLUMN_NAME = 'evidence_summary'),
  'SELECT 1',
  'ALTER TABLE `sys_ai_diagnosis_record` ADD COLUMN `evidence_summary` longtext COMMENT ''证据摘要'' AFTER `executive_summary`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql := IF(
  EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_ai_diagnosis_record' AND COLUMN_NAME = 'action_summary'),
  'SELECT 1',
  'ALTER TABLE `sys_ai_diagnosis_record` ADD COLUMN `action_summary` longtext COMMENT ''建议动作'' AFTER `evidence_summary`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql := IF(
  EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_ai_diagnosis_record' AND COLUMN_NAME = 'risk_summary'),
  'SELECT 1',
  'ALTER TABLE `sys_ai_diagnosis_record` ADD COLUMN `risk_summary` longtext COMMENT ''风险评估'' AFTER `action_summary`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql := IF(
  EXISTS(SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_ai_diagnosis_record' AND COLUMN_NAME = 'report_markdown'),
  'SELECT 1',
  'ALTER TABLE `sys_ai_diagnosis_record` ADD COLUMN `report_markdown` longtext COMMENT ''报告Markdown'' AFTER `risk_summary`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
INSERT INTO `sys_ai_diagnostic_tool_config`
(`uuid`, `scene_code`, `tool_code`, `tool_name`, `enabled_flag`, `priority`, `tool_prompt`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514315', 'system_diagnose', 'warehouse_summary', '库存摘要', 1, 10, '结合库存摘要判断库位状态、库存结构与重点物料分布。', 1, 0, 1, 2, NOW(), 2, NOW(), '默认诊断工具'
FROM DUAL WHERE NOT EXISTS (
  SELECT 1 FROM `sys_ai_diagnostic_tool_config` WHERE `tenant_id` = 1 AND `scene_code` = 'system_diagnose' AND `tool_code` = 'warehouse_summary'
);
INSERT INTO `sys_ai_diagnostic_tool_config`
(`uuid`, `scene_code`, `tool_code`, `tool_name`, `enabled_flag`, `priority`, `tool_prompt`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514316', 'system_diagnose', 'task_summary', '任务摘要', 1, 20, '结合任务摘要识别积压、异常状态和最近变更任务。', 1, 0, 1, 2, NOW(), 2, NOW(), '默认诊断工具'
FROM DUAL WHERE NOT EXISTS (
  SELECT 1 FROM `sys_ai_diagnostic_tool_config` WHERE `tenant_id` = 1 AND `scene_code` = 'system_diagnose' AND `tool_code` = 'task_summary'
);
INSERT INTO `sys_ai_diagnostic_tool_config`
(`uuid`, `scene_code`, `tool_code`, `tool_name`, `enabled_flag`, `priority`, `tool_prompt`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514317', 'system_diagnose', 'device_site_summary', '设备站点摘要', 1, 30, '结合设备站点摘要判断设备状态、巷道分布和最近更新站点。', 1, 0, 1, 2, NOW(), 2, NOW(), '默认诊断工具'
FROM DUAL WHERE NOT EXISTS (
  SELECT 1 FROM `sys_ai_diagnostic_tool_config` WHERE `tenant_id` = 1 AND `scene_code` = 'system_diagnose' AND `tool_code` = 'device_site_summary'
);
INSERT INTO `sys_ai_diagnostic_tool_config`
(`uuid`, `scene_code`, `tool_code`, `tool_name`, `enabled_flag`, `priority`, `tool_prompt`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514318', 'system_diagnose', 'operation_record', '异常操作日志', 1, 40, '重点识别最近失败操作和高频异常。', 1, 0, 1, 2, NOW(), 2, NOW(), '默认诊断工具'
FROM DUAL WHERE NOT EXISTS (
  SELECT 1 FROM `sys_ai_diagnostic_tool_config` WHERE `tenant_id` = 1 AND `scene_code` = 'system_diagnose' AND `tool_code` = 'operation_record'
);
INSERT INTO `sys_ai_diagnostic_tool_config`
(`uuid`, `scene_code`, `tool_code`, `tool_name`, `enabled_flag`, `priority`, `tool_prompt`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514319', 'system_diagnose', 'ai_call_failure', 'AI调用失败', 1, 50, '重点识别最近 AI 调用失败的模型、错误类型和时间窗口。', 1, 0, 1, 2, NOW(), 2, NOW(), '默认诊断工具'
FROM DUAL WHERE NOT EXISTS (
  SELECT 1 FROM `sys_ai_diagnostic_tool_config` WHERE `tenant_id` = 1 AND `scene_code` = 'system_diagnose' AND `tool_code` = 'ai_call_failure'
);
SET @system_menu_ai_tool_config := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiToolConfig' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiToolConfig', 1, 'menu.system', '1', 'menu.system', '/system/aiToolConfig', 'aiToolConfig', NULL, NULL, 0, NULL, 'BuildCircle', 14, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_tool_config IS NULL;
SET @system_menu_ai_tool_config := COALESCE(@system_menu_ai_tool_config, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiToolConfig' LIMIT 1));
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiToolConfig', @system_menu_ai_tool_config, NULL, CONCAT('1,', @system_menu_ai_tool_config), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiToolConfig:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_tool_config AND `authority` = 'system:aiToolConfig:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiToolConfig', @system_menu_ai_tool_config, NULL, CONCAT('1,', @system_menu_ai_tool_config), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiToolConfig:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_tool_config AND `authority` = 'system:aiToolConfig:save');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiToolConfig', @system_menu_ai_tool_config, NULL, CONCAT('1,', @system_menu_ai_tool_config), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiToolConfig:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_tool_config AND `authority` = 'system:aiToolConfig:update');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiToolConfig', @system_menu_ai_tool_config, NULL, CONCAT('1,', @system_menu_ai_tool_config), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiToolConfig:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_tool_config AND `authority` = 'system:aiToolConfig:remove');
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `authority` IN ('system:aiToolConfig:list', 'system:aiToolConfig:save', 'system:aiToolConfig:update', 'system:aiToolConfig:remove')
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `component` IN ('aiToolConfig')
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_phase4.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `sys_ai_diagnosis_plan` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `uuid` varchar(64) DEFAULT NULL COMMENT '编号',
  `plan_name` varchar(255) DEFAULT NULL COMMENT '计划名称',
  `scene_code` varchar(64) DEFAULT NULL COMMENT '场景编码',
  `cron_expr` varchar(128) DEFAULT NULL COMMENT 'Cron表达式',
  `prompt` longtext COMMENT '巡检提示词',
  `preferred_model_code` varchar(128) DEFAULT NULL COMMENT '优先模型编码',
  `running_flag` int(1) NOT NULL DEFAULT '0' COMMENT '运行中{1:是,0:否}',
  `last_result` int(1) DEFAULT NULL COMMENT '上次结果{2:运行中,1:成功,0:失败}',
  `last_diagnosis_id` bigint(20) DEFAULT NULL COMMENT '上次诊断记录ID',
  `last_run_time` datetime DEFAULT NULL COMMENT '上次运行时间',
  `next_run_time` datetime DEFAULT NULL COMMENT '下次运行时间',
  `last_message` varchar(1000) DEFAULT NULL COMMENT '最近消息',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_ai_diag_plan_due` (`tenant_id`,`status`,`running_flag`,`next_run_time`,`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI诊断巡检计划';
SET @system_menu_ai_plan := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiDiagnosisPlan' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiDiagnosisPlan', 1, 'menu.system', '1', 'menu.system', '/system/aiDiagnosisPlan', 'aiDiagnosisPlan', NULL, NULL, 0, NULL, 'Schedule', 14, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_plan IS NULL;
SET @system_menu_ai_plan := COALESCE(@system_menu_ai_plan, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiDiagnosisPlan' LIMIT 1));
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiDiagnosisPlan', @system_menu_ai_plan, NULL, CONCAT('1,', @system_menu_ai_plan), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiDiagnosisPlan:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_plan AND `authority` = 'system:aiDiagnosisPlan:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiDiagnosisPlan', @system_menu_ai_plan, NULL, CONCAT('1,', @system_menu_ai_plan), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiDiagnosisPlan:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_plan AND `authority` = 'system:aiDiagnosisPlan:save');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiDiagnosisPlan', @system_menu_ai_plan, NULL, CONCAT('1,', @system_menu_ai_plan), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiDiagnosisPlan:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_plan AND `authority` = 'system:aiDiagnosisPlan:update');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiDiagnosisPlan', @system_menu_ai_plan, NULL, CONCAT('1,', @system_menu_ai_plan), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiDiagnosisPlan:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_plan AND `authority` = 'system:aiDiagnosisPlan:remove');
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `authority` IN (
    'system:aiDiagnosisPlan:list', 'system:aiDiagnosisPlan:save', 'system:aiDiagnosisPlan:update', 'system:aiDiagnosisPlan:remove'
)
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `component` IN ('aiDiagnosisPlan')
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_phase5.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `sys_ai_mcp_mount` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `uuid` varchar(64) DEFAULT NULL COMMENT '编号',
  `name` varchar(255) DEFAULT NULL COMMENT '名称',
  `mount_code` varchar(128) DEFAULT NULL COMMENT '挂载编码',
  `transport_type` varchar(32) DEFAULT NULL COMMENT '传输类型',
  `url` varchar(512) DEFAULT NULL COMMENT '地址',
  `enabled_flag` int(1) NOT NULL DEFAULT '1' COMMENT '启用{1:是,0:否}',
  `timeout_ms` int(11) DEFAULT NULL COMMENT '超时毫秒',
  `last_test_result` int(1) DEFAULT NULL COMMENT '上次测试结果{1:成功,0:失败}',
  `last_test_time` datetime DEFAULT NULL COMMENT '上次测试时间',
  `last_test_message` varchar(1000) DEFAULT NULL COMMENT '上次测试消息',
  `last_tool_count` int(11) DEFAULT NULL COMMENT '上次工具数',
  `status` int(1) NOT NULL DEFAULT '1' COMMENT '状态{1:正常,0:冻结}',
  `deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除{1:是,0:否}',
  `tenant_id` bigint(20) DEFAULT NULL COMMENT '租户ID',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  KEY `idx_ai_mcp_mount` (`tenant_id`,`mount_code`,`status`,`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI MCP挂载';
INSERT INTO `sys_ai_mcp_mount`
(`uuid`, `name`, `mount_code`, `transport_type`, `url`, `enabled_flag`, `timeout_ms`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
SELECT '6702082748514325', 'WMS本地MCP', 'wms_local', 'INTERNAL', '/ai/mcp', 1, 10000, 1, 0, 1, 2, NOW(), 2, NOW(), '默认挂载当前 WMS AI 内置工具集合'
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 FROM `sys_ai_mcp_mount` WHERE `tenant_id` = 1 AND `mount_code` = 'wms_local'
);
SET @system_menu_ai_mcp_mount := (
    SELECT `id` FROM `sys_menu` WHERE `component` = 'aiMcpMount' LIMIT 1
);
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'menu.aiMcpMount', 1, 'menu.system', '1', 'menu.system', '/system/aiMcpMount', 'aiMcpMount', NULL, NULL, 0, NULL, 'Hub', 15, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE @system_menu_ai_mcp_mount IS NULL;
SET @system_menu_ai_mcp_mount := COALESCE(@system_menu_ai_mcp_mount, LAST_INSERT_ID(), (SELECT `id` FROM `sys_menu` WHERE `component` = 'aiMcpMount' LIMIT 1));
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Query AiMcpMount', @system_menu_ai_mcp_mount, NULL, CONCAT('1,', @system_menu_ai_mcp_mount), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiMcpMount:list', NULL, 0, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_mcp_mount AND `authority` = 'system:aiMcpMount:list');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Create AiMcpMount', @system_menu_ai_mcp_mount, NULL, CONCAT('1,', @system_menu_ai_mcp_mount), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiMcpMount:save', NULL, 1, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_mcp_mount AND `authority` = 'system:aiMcpMount:save');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Update AiMcpMount', @system_menu_ai_mcp_mount, NULL, CONCAT('1,', @system_menu_ai_mcp_mount), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiMcpMount:update', NULL, 2, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_mcp_mount AND `authority` = 'system:aiMcpMount:update');
INSERT INTO `sys_menu`
(`name`, `parent_id`, `parent_name`, `path`, `path_name`, `route`, `component`, `brief`, `code`, `type`, `authority`, `icon`, `sort`, `meta`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)
SELECT 'Delete AiMcpMount', @system_menu_ai_mcp_mount, NULL, CONCAT('1,', @system_menu_ai_mcp_mount), NULL, NULL, NULL, NULL, NULL, 1, 'system:aiMcpMount:remove', NULL, 3, NULL, 1, 1, 0, NOW(), 2, NOW(), 2, NULL
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `sys_menu` WHERE `parent_id` = @system_menu_ai_mcp_mount AND `authority` = 'system:aiMcpMount:remove');
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `authority` IN (
    'system:aiMcpMount:list', 'system:aiMcpMount:save', 'system:aiMcpMount:update', 'system:aiMcpMount:remove'
)
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, `id` FROM `sys_menu`
WHERE `component` IN ('aiMcpMount')
AND NOT EXISTS (
    SELECT 1 FROM `sys_role_menu`
    WHERE `role_id` = 1 AND `menu_id` = `sys_menu`.`id`
);
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_phase6_mcp_console.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET @table_schema = DATABASE();
SET @add_auth_type_sql = (
    SELECT IF(
        COUNT(*) = 0,
        'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `auth_type` varchar(32) DEFAULT ''NONE'' COMMENT ''认证方式'' AFTER `url`',
        'SELECT 1'
    )
    FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = @table_schema
      AND TABLE_NAME = 'sys_ai_mcp_mount'
      AND COLUMN_NAME = 'auth_type'
);
PREPARE stmt FROM @add_auth_type_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @add_auth_value_sql = (
    SELECT IF(
        COUNT(*) = 0,
        'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `auth_value` varchar(512) DEFAULT NULL COMMENT ''认证信息'' AFTER `auth_type`',
        'SELECT 1'
    )
    FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = @table_schema
      AND TABLE_NAME = 'sys_ai_mcp_mount'
      AND COLUMN_NAME = 'auth_value'
);
PREPARE stmt FROM @add_auth_value_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @add_usage_scope_sql = (
    SELECT IF(
        COUNT(*) = 0,
        'ALTER TABLE `sys_ai_mcp_mount` ADD COLUMN `usage_scope` varchar(32) DEFAULT ''DIAGNOSE_ONLY'' COMMENT ''用途范围'' AFTER `auth_value`',
        'SELECT 1'
    )
    FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = @table_schema
      AND TABLE_NAME = 'sys_ai_mcp_mount'
      AND COLUMN_NAME = 'usage_scope'
);
PREPARE stmt FROM @add_usage_scope_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `sys_ai_mcp_mount`
SET `auth_type` = COALESCE(NULLIF(`auth_type`, ''), 'NONE'),
    `usage_scope` = CASE
        WHEN `transport_type` = 'INTERNAL' THEN 'CHAT_AND_DIAGNOSE'
        ELSE COALESCE(NULLIF(`usage_scope`, ''), 'DIAGNOSE_ONLY')
    END
WHERE 1 = 1;
SET FOREIGN_KEY_CHECKS = 1;
-- >>> 20260316_ai_phase7_tool_usage_scope.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
SET @table_schema = DATABASE();
SET @add_usage_scope_sql = (
    SELECT IF(
        COUNT(*) = 0,
        'ALTER TABLE `sys_ai_diagnostic_tool_config` ADD COLUMN `usage_scope` varchar(32) DEFAULT ''DIAGNOSE_ONLY'' COMMENT ''用途范围'' AFTER `tool_prompt`',
        'SELECT 1'
    )
    FROM information_schema.COLUMNS
    WHERE TABLE_SCHEMA = @table_schema
      AND TABLE_NAME = 'sys_ai_diagnostic_tool_config'
      AND COLUMN_NAME = 'usage_scope'
);
PREPARE stmt FROM @add_usage_scope_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `sys_ai_diagnostic_tool_config`
SET `usage_scope` = CASE
    WHEN `enabled_flag` IS NOT NULL AND `enabled_flag` <> 1 THEN 'DISABLED'
    WHEN `scene_code` IS NULL OR TRIM(`scene_code`) = '' THEN 'CHAT_AND_DIAGNOSE'
    ELSE 'DIAGNOSE_ONLY'
END
WHERE `usage_scope` IS NULL OR TRIM(`usage_scope`) = '';
SET FOREIGN_KEY_CHECKS = 1;
version/db/init.sql
@@ -77,14 +77,13 @@
  PRIMARY KEY (`id`),
  KEY `idx_ai_param_model_code` (`model_code`),
  KEY `idx_ai_param_deleted_code` (`deleted`,`model_code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_ai_param
-- ----------------------------
BEGIN;
INSERT INTO `sys_ai_param` (`id`, `uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`) VALUES (1, '6702082748514305', '通用助手', 'mock-general', 'mock', NULL, NULL, 'mock-general', '你是WMS系统内的智能助手,回答时优先保持准确、简洁,并结合上下文帮助用户理解仓储业务。', 12, 1, 1, 1, 0, 1, 2, '2025-02-05 14:16:51', 2, '2025-02-05 14:16:51', '默认演示模型');
INSERT INTO `sys_ai_param` (`id`, `uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`) VALUES (2, '6702082748514306', '创意助手', 'mock-creative', 'mock', NULL, NULL, 'mock-creative', '你是WMS系统内的智能助手,回答时可以更灵活地组织表达,但结论必须准确。', 12, 0, 2, 1, 0, 1, 2, '2025-02-05 14:16:51', 2, '2025-02-05 14:16:51', '演示创意模型');
INSERT INTO `sys_ai_param` (`id`, `uuid`, `name`, `model_code`, `provider`, `chat_url`, `api_key`, `model_name`, `system_prompt`, `max_context_messages`, `default_flag`, `sort`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`) VALUES (1, '6702082748514305', 'DEEPSEEK', 'deepseek-ai/DeepSeek-V3.2', 'openai', 'https://api.siliconflow.cn', NULL, 'deepseek-ai/DeepSeek-V3.2', '你是WMS系统内的智能助手,回答时优先保持准确、简洁,并结合上下文帮助用户理解仓储业务。', 12, 1, 1, 1, 0, 1, 2, '2026-03-11 14:13:22', 2, '2026-03-11 15:03:30', '默认演示模型');
COMMIT;
-- ----------------------------