3 天以前 83e1b4d0e661f11a407fd6ea86e906b9b87b7180
feat(aftersales): 售后工单新增问题类型和严重程度字段

- 在AfterSaleTicketSaveReqVO中新增issueType字段用于标识问题类型
- 在AfterSaleTicketSaveReqVO中新增issueCategory字段用于问题分类
- 在AfterSaleTicketSaveReqVO中新增severityLevel字段用于标识严重程度
- 完善了问题类型的枚举定义:1-一般问题 2-维修问题 3-退货问题
- 完善了严重程度的枚举定义:0-轻微 1-一般 2-严重 3-致命

docs(ai): 新增AI知识库模块接口文档

- 创建了完整的AI知识库模块API文档
- 包含知识库、文档、分段三个核心功能的接口说明
- 整理了模型API的相关接口函数和使用位置
- 提供了详细的函数方法、路径和使用位置对照表

refactor(ai): 移除AI API密钥相关代码

- 删除了AiApiKeyController控制器类及其相关接口
- 移除了AiApiKeyDO数据对象和对应的数据库映射
- 删除了AiApiKeyService服务接口及其实现类
- 清理了API密钥相关的请求响应VO类
- 移除了AI自动配置中的相关组件注册
已修改67个文件
已添加53个文件
已删除174个文件
21742 ■■■■ 文件已修改
docs/ai_knowledge_api(1).md 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/ai_knowledge_rag_integration.md 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_sale_quotation_ocr_integration.md 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_pro_ai_integration.md 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_qc_ai_integration.md 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/oa_notice_frontend.md 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/test-quotation-files/南通客户报价单.csv 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/test-quotation-files/测试客户报价单.txt 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/test-quotation-files/聊天记录生成提示词.txt 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/oa_notice_init.sql 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApi.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/pom.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java 81 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java 92 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateReqVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSplitReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSplitRespVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.http 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowPageReqVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowRespVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowSaveReqVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/workflow/AiWorkflowDO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatRoleMapper.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/workflow/AiWorkflowMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/AiDocumentSplitStrategyEnum.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java 52 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/midjourney/api/MidjourneyApi.java 351 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowApiConstants.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageApi.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageOptions.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/suno/api/SunoApi.java 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/wenduoduo/api/WenDuoDuoPptApi.java 381 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/api/XunFeiPptApi.java 522 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/package-info.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/package-info.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/web/config/AiWebConfiguration.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/job/image/AiMidjourneySyncJob.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java 572 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java 365 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/splitter/MarkdownQaSplitter.java 262 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/splitter/SemanticTextSplitter.java 225 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java 74 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java 117 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowService.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/FileTypeUtils.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/org/springframework/ai/model/tool/DefaultToolExecutionEligibilityPredicate.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/main/java/org/springframework/ai/model/tool/ToolExecutionEligibilityPredicate.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/CozeChatModelTests.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DifyChatModelTests.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/FastGPTChatModelTests.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/MidjourneyApiTests.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/SiliconFlowImageModelTests.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/TongYiImagesModelTest.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/mcp/DouBaoMcpTests.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/music/SunoApiTests.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/ppt/wdd/WenDuoDuoPptApiTests.java 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/ppt/xunfei/XunFeiPptApiTests.java 319 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOANoticeController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/OaNoticePageReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/OaNoticeRespVO.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/OaNoticeSaveReqVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/OaNoticeDO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/OaNoticeReadDO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/OaNoticeUserDO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/OaNoticeMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/OaNoticeReadMapper.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/OaNoticeUserMapper.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/oa/OaNoticeSendScopeEnum.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/oa/OaNoticeStatusEnum.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/oa/OaNoticeTypeEnum.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/OaNoticeService.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/OaNoticeServiceImpl.java 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/pom.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/ai/CrmSaleQuotationAiController.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/ai/vo/CrmSaleQuotationOcrReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/ai/vo/CrmSaleQuotationOcrRespVO.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/ai/CrmSaleQuotationAiService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/ai/CrmSaleQuotationAiServiceImpl.java 281 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/MesProAiController.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDeliveryPredictReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDeliveryPredictRespVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDurationPredictReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDurationPredictRespVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiMaterialPredictReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiMaterialPredictRespVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiRiskPredictReqVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiRiskPredictRespVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ai/MesQcAiController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ai/vo/MesQcAiSuggestReqVO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ai/vo/MesQcAiSuggestRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/indicatorresult/MesQcIndicatorResultMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java 782 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ai/MesQcAiService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ai/MesQcAiServiceImpl.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyMessageMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/pom.xml 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-dev.yaml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-local.yaml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-test.yaml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application.yaml 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/ai_knowledge_api(1).md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
# AI çŸ¥è¯†åº“模块 - æŽ¥å£æ–‡æ¡£
## æ¶‰åŠé¡µé¢
```
src/views/ai/knowledge/knowledge/index.vue                # çŸ¥è¯†åº“列表
src/views/ai/knowledge/knowledge/modules/form.vue         # çŸ¥è¯†åº“表单弹窗
src/views/ai/knowledge/knowledge/data.ts                  # çŸ¥è¯†åº“配置
src/views/ai/knowledge/knowledge/retrieval/index.vue       # å¬å›žæµ‹è¯•
src/views/ai/knowledge/document/index.vue                 # æ–‡æ¡£åˆ—表
src/views/ai/knowledge/document/data.ts                   # æ–‡æ¡£é…ç½®
src/views/ai/knowledge/document/form/index.vue            # æ–‡æ¡£åˆ›å»º/编辑(步骤页)
src/views/ai/knowledge/document/form/modules/upload-step.vue   # ä¸Šä¼ æ­¥éª¤
src/views/ai/knowledge/document/form/modules/split-step.vue    # åˆ†æ®µæ­¥éª¤
src/views/ai/knowledge/document/form/modules/process-step.vue  # å¤„理步骤
src/views/ai/knowledge/segment/index.vue                  # åˆ†æ®µåˆ—表
src/views/ai/knowledge/segment/data.ts                    # åˆ†æ®µé…ç½®
src/views/ai/knowledge/segment/modules/form.vue           # åˆ†æ®µè¡¨å•弹窗
```
---
## çŸ¥è¯†åº“ API(`src/api/ai/knowledge/knowledge/index.ts`)
| å‡½æ•° | æ–¹æ³• | è·¯å¾„ | ä½¿ç”¨ä½ç½® |
|------|------|------|----------|
| `getKnowledgePage` | GET | `/ai/knowledge/page` | knowledge/index.vue |
| `getKnowledge` | GET | `/ai/knowledge/get?id={id}` | knowledge/form.vue、knowledge/retrieval/index.vue |
| `createKnowledge` | POST | `/ai/knowledge/create` | knowledge/form.vue |
| `updateKnowledge` | PUT | `/ai/knowledge/update` | knowledge/form.vue |
| `deleteKnowledge` | DELETE | `/ai/knowledge/delete?id={id}` | knowledge/index.vue |
## æ–‡æ¡£ API(`src/api/ai/knowledge/document/index.ts`)
| å‡½æ•° | æ–¹æ³• | è·¯å¾„ | ä½¿ç”¨ä½ç½® |
|------|------|------|----------|
| `getKnowledgeDocumentPage` | GET | `/ai/knowledge/document/page` | document/index.vue |
| `getKnowledgeDocument` | GET | `/ai/knowledge/document/get?id={id}` | document/form/index.vue |
| `createKnowledgeDocumentList` | POST | `/ai/knowledge/document/create-list` | document/form/modules/split-step.vue |
| `updateKnowledgeDocument` | PUT | `/ai/knowledge/document/update` | document/form/modules/split-step.vue |
| `updateKnowledgeDocumentStatus` | PUT | `/ai/knowledge/document/update-status` | document/index.vue |
| `deleteKnowledgeDocument` | DELETE | `/ai/knowledge/document/delete?id={id}` | document/index.vue |
| `processDocument` | POST | `/ai/knowledge/document/process/{id}` | document/index.vue |
## åˆ†æ®µ API(`src/api/ai/knowledge/segment/index.ts`)
| å‡½æ•° | æ–¹æ³• | è·¯å¾„ | ä½¿ç”¨ä½ç½® |
|------|------|------|----------|
| `getKnowledgeSegmentPage` | GET | `/ai/knowledge/segment/page` | segment/index.vue |
| `getKnowledgeSegment` | GET | `/ai/knowledge/segment/get?id={id}` | segment/modules/form.vue |
| `createKnowledgeSegment` | POST | `/ai/knowledge/segment/create` | segment/modules/form.vue |
| `updateKnowledgeSegment` | PUT | `/ai/knowledge/segment/update` | segment/modules/form.vue |
| `updateKnowledgeSegmentStatus` | PUT | `/ai/knowledge/segment/update-status` | segment/index.vue |
| `deleteKnowledgeSegment` | DELETE | `/ai/knowledge/segment/delete?id={id}` | segment/index.vue |
| `splitContent` | GET | `/ai/knowledge/segment/split` | document/form/modules/split-step.vue |
| `getKnowledgeSegmentProcessList` | GET | `/ai/knowledge/segment/get-process-list` | document/form/modules/process-step.vue |
| `searchKnowledgeSegment` | POST | `/ai/knowledge/segment/search` | knowledge/retrieval/index.vue |
## æ¨¡åž‹ API(`src/api/ai/model/model/index.ts`)
| å‡½æ•° | æ–¹æ³• | è·¯å¾„ | ä½¿ç”¨ä½ç½® |
|------|------|------|----------|
| `getModelSimpleList` | GET | `/ai/model/simple-list?type={type}` | knowledge/data.ts |
---
> å…± 4 ä¸ª API æ–‡ä»¶ï¼Œ22 ä¸ªæŽ¥å£å‡½æ•°
docs/ai_knowledge_rag_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,373 @@
# AI çŸ¥è¯†åº“ RAG åŠŸèƒ½ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- **API å¯†é’¥ç®¡ç†**:`ai/views/apikey/` â€” ç®¡ç† AI å¹³å° API å¯†é’¥
- **模型管理**:`ai/views/model/` â€” ç®¡ç† AI æ¨¡åž‹é…ç½®ï¼ˆèŠå¤©ã€å‘量化等)
- **知识库管理**:`ai/views/knowledge/` â€” çŸ¥è¯†åº“ CRUD、文档上传、分段管理、RAG æ£€ç´¢
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
```
æ­¥1: é…ç½® API å¯†é’¥ï¼ˆä¸€æ¬¡æ€§ï¼‰
  â””─ åœ¨ã€ŒAPI å¯†é’¥ç®¡ç†ã€é¡µé…ç½®é€šä¹‰åƒé—® API Key â†’ èŽ·å¾— apiKeyId
æ­¥2: é…ç½®å‘量化模型(一次性)
  â””─ åœ¨ã€Œæ¨¡åž‹ç®¡ç†ã€é¡µæ–°å»º Embedding æ¨¡åž‹ â†’ keyId ä»Žæ­¥1带入
  â””─ æ¨¡åž‹æ ‡è¯†å¡« "text-embedding-v3",平台选 "TongYi",类型选 "EMBEDDING(5)"
  â””─ èŽ·å¾— embeddingModelId
æ­¥3: åˆ›å»ºçŸ¥è¯†åº“
  â””─ åœ¨ã€ŒçŸ¥è¯†åº“管理」页新建知识库 â†’ embeddingModelId ä»Žæ­¥2带入
  â””─ å¯è®¾ç½® topK å’Œç›¸ä¼¼åº¦é˜ˆå€¼
æ­¥4: åˆ›å»ºæ–‡æ¡£
  â””─ åœ¨çŸ¥è¯†åº“详情页上传文档(提供名称和 URL)
  â””─ åŽç«¯è‡ªåŠ¨ä¸‹è½½æ–‡æ¡£ã€æå–æ–‡æœ¬å†…å®¹ï¼ˆTika è§£æžï¼‰
æ­¥5: å¤„理文档(分段+向量化)
  â””─ åœ¨æ–‡æ¡£åˆ—表点击「处理」按钮
  â””─ åŽç«¯å°†æ–‡æ¡£å†…容切片、调用向量模型生成 Embedding、写入 Milvus
æ­¥6: RAG æ£€ç´¢
  â””─ åœ¨çŸ¥è¯†åº“详情页输入检索内容 â†’ åŽç«¯å‘量检索 + ç›¸ä¼¼åº¦è¿‡æ»¤
  â””─ è¿”回最匹配的分段及相似度分数
```
---
## API æŽ¥å£
### 1. API å¯†é’¥ç®¡ç†
**基础路径**:`/ai/api-key`
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/ai/api-key/create` | åˆ›å»º API å¯†é’¥ |
| PUT | `/ai/api-key/update` | æ›´æ–° API å¯†é’¥ |
| DELETE | `/ai/api-key/delete` | åˆ é™¤ API å¯†é’¥ |
| GET | `/ai/api-key/get` | èŽ·å–å•ä¸ª API å¯†é’¥ |
| GET | `/ai/api-key/page` | åˆ†é¡µæŸ¥è¯¢ API å¯†é’¥ |
**POST /create è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| name | String | æ˜¯ | å¯†é’¥åç§°ï¼Œå¦‚"通义千问" |
| apiKey | String | æ˜¯ | API å¯†é’¥å­—符串 |
| platform | String | æ˜¯ | å¹³å°ï¼š`TongYi`(通义千问) |
| url | String | å¦ | API åœ°å€ï¼ˆä¸€èˆ¬ç•™ç©ºï¼‰ |
| status | Integer | å¦ | çŠ¶æ€ï¼š0=启用,1=禁用 |
**响应:** `{ "code": 0, "data": 1 }` (返回密钥 ID)
---
### 2. æ¨¡åž‹ç®¡ç†
**基础路径**:`/ai/model`
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/ai/model/create` | åˆ›å»º AI æ¨¡åž‹ |
| PUT | `/ai/model/update` | æ›´æ–° AI æ¨¡åž‹ |
| DELETE | `/ai/model/delete` | åˆ é™¤ AI æ¨¡åž‹ |
| GET | `/ai/model/get` | èŽ·å–å•ä¸ªæ¨¡åž‹ |
| GET | `/ai/model/page` | åˆ†é¡µæŸ¥è¯¢æ¨¡åž‹ |
**POST /create è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| keyId | Long | æ˜¯ | API å¯†é’¥ ID(从上一步带入) |
| name | String | æ˜¯ | æ¨¡åž‹åç§°ï¼Œå¦‚"通义千问向量化" |
| model | String | æ˜¯ | æ¨¡åž‹æ ‡è¯†ï¼Œå‘量化模型填 `text-embedding-v3` |
| platform | String | æ˜¯ | å¹³å°ï¼Œå›ºå®š `TongYi` |
| type | Integer | æ˜¯ | æ¨¡åž‹ç±»åž‹ï¼š1=CHAT,5=EMBEDDING |
| sort | Integer | å¦ | æŽ’序 |
| status | Integer | å¦ | çŠ¶æ€ï¼š0=启用 |
| temperature | Double | å¦ | æ¸©åº¦ï¼ˆEmbedding ç±»åž‹ä¸éœ€è¦ï¼‰ |
| maxTokens | Integer | å¦ | æœ€å¤§ Token |
| maxContexts | Integer | å¦ | æœ€å¤§ä¸Šä¸‹æ–‡ |
**模型类型枚举:**
| type å€¼ | è¯´æ˜Ž |
|---------|------|
| 1 | CHAT(聊天) |
| 5 | EMBEDDING(向量化) |
**平台枚举:**
| platform | è¯´æ˜Ž |
|----------|------|
| TongYi | é€šä¹‰åƒé—® |
---
### 3. çŸ¥è¯†åº“管理
**基础路径**:`/ai/knowledge`
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/ai/knowledge/create` | åˆ›å»ºçŸ¥è¯†åº“ |
| PUT | `/ai/knowledge/update` | æ›´æ–°çŸ¥è¯†åº“ |
| DELETE | `/ai/knowledge/delete` | åˆ é™¤çŸ¥è¯†åº“ |
| GET | `/ai/knowledge/get` | èŽ·å–å•ä¸ªçŸ¥è¯†åº“ |
| GET | `/ai/knowledge/page` | åˆ†é¡µæŸ¥è¯¢çŸ¥è¯†åº“ |
| POST | `/ai/knowledge/document/create` | åˆ›å»ºæ–‡æ¡£ï¼ˆæ‰¹é‡ï¼‰ |
**POST /create è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| name | String | æ˜¯ | çŸ¥è¯†åº“名称 |
| description | String | å¦ | çŸ¥è¯†åº“描述 |
| embeddingModelId | Long | æ˜¯ | å‘量模型 ID(从模型管理页带入) |
| topK | Integer | å¦ | æ£€ç´¢è¿”回条数,默认 3 |
| similarityThreshold | Double | å¦ | ç›¸ä¼¼åº¦é˜ˆå€¼ï¼Œé»˜è®¤ 0.7 |
| status | Integer | å¦ | çŠ¶æ€ï¼š0=启用 |
**提示**:创建知识库前,需要先有一个 `type=5 (EMBEDDING)` çš„æ¨¡åž‹ã€‚前端应在选择向量模型时过滤只显示 `type=5` çš„æ¨¡åž‹ã€‚
**POST /document/create è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| knowledgeId | Long | æ˜¯ | çŸ¥è¯†åº“ ID |
| names | List\<String\> | æ˜¯ | æ–‡æ¡£åç§°åˆ—表,如 `["产品手册.pdf", "API文档.docx"]` |
| urls | List\<String\> | æ˜¯ | æ–‡æ¡£ URL åˆ—表(与 names ä¸€ä¸€å¯¹åº”) |
---
### 4. æ–‡æ¡£ç®¡ç†
**基础路径**:`/ai/knowledge/document`
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | `/ai/knowledge/document/update` | æ›´æ–°æ–‡æ¡£ |
| DELETE | `/ai/knowledge/document/delete` | åˆ é™¤æ–‡æ¡£ï¼ˆå«åˆ†æ®µ+向量) |
| GET | `/ai/knowledge/document/get` | èŽ·å–å•ä¸ªæ–‡æ¡£ |
| GET | `/ai/knowledge/document/page` | åˆ†é¡µæŸ¥è¯¢æ–‡æ¡£ |
| PUT | `/ai/knowledge/document/update-status` | æ›´æ–°æ–‡æ¡£çŠ¶æ€ |
| POST | `/ai/knowledge/document/process/{id}` | å¤„理文档(分段+向量化) |
| GET | `/ai/knowledge/document/progress` | æŸ¥è¯¢æ–‡æ¡£å¤„理进度 |
**PUT /update è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | æ–‡æ¡£ ID |
| name | String | å¦ | æ–‡æ¡£åç§° |
| url | String | å¦ | æ–‡æ¡£ URL |
**POST /process/{id}**:将文档内容切片并写入 Milvus å‘量库。处理完才能被检索到。
**GET /progress è¯·æ±‚参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| documentIds | List\<Long\> | æ˜¯ | æ–‡æ¡£ ID åˆ—表 |
**GET /progress å“åº”新增字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| documentId | Long | æ–‡æ¡£ ID |
| count | Long | åˆ†æ®µæ€»æ•° |
| embeddingCount | Long | å·²å‘量化数量 |
---
### 5. åˆ†æ®µç®¡ç† & RAG æ£€ç´¢
**基础路径**:`/ai/knowledge/segment`
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/ai/knowledge/segment/get` | èŽ·å–å•ä¸ªåˆ†æ®µ |
| GET | `/ai/knowledge/segment/page` | åˆ†é¡µæŸ¥è¯¢åˆ†æ®µ |
| **GET** | **`/ai/knowledge/segment/split`** | **文档切分预览(处理前预览切分效果)** |
| PUT | `/ai/knowledge/segment/update-status` | æ›´æ–°åˆ†æ®µçŠ¶æ€ |
| **POST** | **`/ai/knowledge/segment/search`** | **RAG å‘量检索(核心接口)** |
### GET /ai/knowledge/segment/split
用于在文档处理前预览切分效果,不保存任何数据。
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| url | String | æ˜¯ | æ–‡æ¡£ URL |
| name | String | æ˜¯ | æ–‡æ¡£åç§°ï¼ˆç”¨äºŽåˆ¤æ–­æ–‡ä»¶ç±»åž‹ï¼‰ |
| segmentMaxTokens | Integer | å¦ | åˆ†æ®µæœ€å¤§ Token æ•°ï¼Œé»˜è®¤ 800 |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    {
      "content": "第一段内容...",
      "contentLength": 256,
      "tokens": 180
    },
    {
      "content": "第二段内容...",
      "contentLength": 300,
      "tokens": 210
    }
  ]
}
```
---
## RAG æ£€ç´¢ï¼ˆæ ¸å¿ƒåŠŸèƒ½ï¼‰
### POST /ai/knowledge/segment/search
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| knowledgeId | Long | æ˜¯ | çŸ¥è¯†åº“ ID |
| content | String | æ˜¯ | æ£€ç´¢å†…容(自然语言) |
| topK | Integer | å¦ | è¿”回条数,默认取知识库配置的 topK |
| similarityThreshold | Double | å¦ | ç›¸ä¼¼åº¦é˜ˆå€¼ï¼Œé»˜è®¤å–知识库配置的阈值 |
**响应:**
```json
{
  "code": 0,
  "data": [
    {
      "id": 100,
      "documentId": 10,
      "knowledgeId": 1,
      "content": "分段内容文本...",
      "contentLength": 256,
      "tokens": 180,
      "score": 0.85
    }
  ]
}
```
**响应字段说明:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | åˆ†æ®µ ID |
| documentId | Long | æ‰€å±žæ–‡æ¡£ ID |
| knowledgeId | Long | æ‰€å±žçŸ¥è¯†åº“ ID |
| content | String | åˆ†æ®µå†…容文本 |
| contentLength | Integer | å†…容字符长度 |
| tokens | Integer | ä¼°ç®— Token æ•° |
| score | Double | ç›¸ä¼¼åº¦åˆ†æ•°ï¼ˆ0~1),越高越相关 |
---
## å­—段展示规则
### çŸ¥è¯†åº“列表/详情
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| name | åˆ—表、详情 | çŸ¥è¯†åº“名称 |
| description | è¯¦æƒ… | çŸ¥è¯†åº“描述 |
| embeddingModel | åˆ—表、详情 | æ˜¾ç¤ºæ¨¡åž‹æ ‡è¯†å¦‚ `text-embedding-v3` |
| topK | è¯¦æƒ… | æ£€ç´¢è¿”回最大条数 |
| similarityThreshold | è¯¦æƒ… | ç›¸ä¼¼åº¦é˜ˆå€¼ï¼ˆ0~1) |
| status | åˆ—表、详情 | å¯ç”¨/禁用 |
| createTime | åˆ—表 | åˆ›å»ºæ—¶é—´ |
### æ–‡æ¡£åˆ—表/详情
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| name | åˆ—表、详情 | æ–‡æ¡£åç§° |
| url | è¯¦æƒ… | æ–‡æ¡£ URL |
| contentLength | è¯¦æƒ… | å†…容长度 |
| tokens | è¯¦æƒ… | Token æ•° |
| retrievalCount | è¯¦æƒ… | è¢«æ£€ç´¢å‘½ä¸­æ¬¡æ•° |
| status | åˆ—表(tag) | çŠ¶æ€æ ‡ç­¾ |
### åˆ†æ®µåˆ—表
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| documentId | åˆ—表 | æ‰€å±žæ–‡æ¡£ ID |
| content | åˆ—表(截断) | åˆ†æ®µå†…容(可截断显示前 100 å­—) |
| contentLength | åˆ—表 | å†…容长度 |
| tokens | åˆ—表 | Token æ•° |
| vectorId | åˆ—表/详情 | å‘量 ID(非空表示已向量化) |
| retrievalCount | åˆ—表 | æ£€ç´¢å‘½ä¸­æ¬¡æ•° |
### RAG æ£€ç´¢ç»“æžœ
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| content | ç»“果卡片 | åˆ†æ®µå…¨æ–‡å†…容 |
| score | ç»“果卡片 | ç›¸ä¼¼åº¦åˆ†æ•°ï¼Œå»ºè®®ç”¨è¿›åº¦æ¡æˆ–颜色标识(>0.8 ç»¿è‰²ï¼Œ0.6~0.8 é»„色,<0.6 ç°è‰²ï¼‰ |
| documentId | è¾…助信息 | å¯ç‚¹å‡»è·³è½¬åˆ°æ¥æºæ–‡æ¡£ |
| contentLength | è¾…助信息 | å†…容长度 |
---
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| åˆ›å»ºæ–‡æ¡£ | åŽç«¯è‡ªåŠ¨ä¸‹è½½ URL æ–‡ä»¶å¹¶æå–文本。支持 `txt/md/json/xml/csv/yaml/yml`(直接读取)和其他格式(Tika è§£æžï¼‰ |
| æ–‡æ¡£å¤„理 | å¿…须先 `POST /process/{id}` å¤„理后,文档内容才会被检索到 |
| æ–‡æ¡£åˆ†æ®µ | åŽç«¯ä½¿ç”¨ `TokenTextSplitter` è‡ªåŠ¨åˆ‡ç‰‡ï¼Œé»˜è®¤æ¯æ®µ 800 tokens |
| å‘量写入 | å¤„理文档时自动调用 Embedding æ¨¡åž‹ç”Ÿæˆå‘量并写入 Milvus |
| åˆ é™¤æ–‡æ¡£ | åŒæ—¶åˆ é™¤æ–‡æ¡£è®°å½•、所有分段记录和 Milvus ä¸­å¯¹åº”向量 |
| æ£€ç´¢ç›¸ä¼¼åº¦ | è¿”回结果按相似度降序排列,低于 `similarityThreshold` çš„结果不会被返回 |
| æ£€ç´¢è®¡æ•°å™¨ | æ¯æ¬¡æ£€ç´¢å‘½ä¸­åŽï¼Œè‡ªåŠ¨æ›´æ–°å¯¹åº”åˆ†æ®µå’Œæ–‡æ¡£çš„ `retrievalCount` |
| API å¯†é’¥ | åˆ›å»ºçŸ¥è¯†åº“前,必须先创建 API å¯†é’¥å’Œ Embedding æ¨¡åž‹ |
---
## æŽ¥å£è°ƒç”¨æ—¶åº
### åˆ›å»ºçŸ¥è¯†åº“完整流程
```
1. POST /ai/api-key/create          â†’ apiKeyId
2. POST /ai/model/create            â†’ modelId (type=5, keyId=apiKeyId)
3. POST /ai/knowledge/create        â†’ knowledgeId (embeddingModelId=modelId)
4. POST /ai/knowledge/document/create â†’ documentIds
5. POST /ai/knowledge/document/process/{documentId} â†’ å‘量化
6. GET  /ai/knowledge/document/progress?documentIds=... â†’ æ£€æŸ¥è¿›åº¦
7. POST /ai/knowledge/segment/search â†’ RAG æ£€ç´¢
```
### æ£€ç´¢æµç¨‹ï¼ˆå·²é…ç½®å¥½çš„知识库)
```
1. POST /ai/knowledge/segment/search
   { knowledgeId, content, topK?, similarityThreshold? }
   â†“
   åŽç«¯: æŸ¥è¯¢å‘量 â†’ Milvus相似度检索 â†’ è¿‡æ»¤ä½ŽäºŽé˜ˆå€¼çš„结果 â†’ æ›´æ–°æ£€ç´¢è®¡æ•°
   â†“
   è¿”回搜索结果列表
```
---
## æ³¨æ„äº‹é¡¹
- çŸ¥è¯†åº“创建时必须选择 `type=5 (EMBEDDING)` çš„æ¨¡åž‹ï¼Œå‰ç«¯èŽ·å–æ¨¡åž‹åˆ—è¡¨åŽéœ€è¦è¿‡æ»¤
- æ–‡æ¡£ä¸Šä¼ åŽ**不会自动处理**,需要在文档列表点击「处理」按钮调用 `POST /process/{id}`
- å¤„理进度可通过 `GET /progress` è½®è¯¢æŸ¥è¯¢ï¼ˆ`embeddingCount >= count` è¡¨ç¤ºå¤„理完成)
- æ£€ç´¢çš„ `similarityThreshold` å»ºè®®é»˜è®¤ 0.7,可根据实际效果调整
- `vectorId` ä¸ºç©ºï¼ˆ`""`)的分段表示尚未写入向量库,不会被检索到
- åˆ é™¤çŸ¥è¯†åº“时,后端当前实现只删除知识库记录本身,需确认是否需要级联删除文档和分段
docs/crm_sale_quotation_ocr_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
# CRM é”€å”®æŠ¥ä»·å• AI OCR è¯†åˆ« - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- `saleQuotation/index.vue` â€” æŠ¥ä»·å•列表页,新增"AI è¯†åˆ«å½•å…¥"按钮
- `saleQuotation/form.vue` â€” æŠ¥ä»·å•新增/编辑弹窗,接收 OCR è¯†åˆ«ç»“果预填表单
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. æŠ¥ä»·å•列表页 â†’ ç‚¹å‡»"AI è¯†åˆ«å½•å…¥"按钮 â†’ å¼¹å‡ºä¸Šä¼ å¼¹çª—
2. ä¸Šä¼ å¼¹çª—中:选择文件上传 â†’ è°ƒç”¨ `/admin-api/system/storage-blob/upload` â†’ èŽ·å¾— `blobId`
3. è°ƒç”¨ `/admin-api/crm/sale-quotation/ai/ocr` ä¼ å…¥ `blobId` â†’ èŽ·å¾—è¯†åˆ«ç»“æžœ
4. é¢„览识别结果 â†’ ç”¨æˆ·ç‚¹å‡»"确认并填入表单" â†’ å…³é—­ä¸Šä¼ å¼¹çª— â†’ æ‰“开新增报价单弹窗
5. æ–°å¢žå¼¹çª—中:识别结果预填到对应字段,用户补充完善后保存
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/crm/sale-quotation/ai/ocr` | AI OCR è¯†åˆ«æŠ¥ä»·æ–‡ä»¶ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| blobId | Long | æ˜¯ | å·²ä¸Šä¼ æ–‡ä»¶çš„ blob ID(通过 `/admin-api/system/storage-blob/upload` èŽ·å–ï¼‰ |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| name | String | æŠ¥ä»·å•名称 |
| customerName | String | å®¢æˆ·åç§°ï¼ˆæ–‡æœ¬ï¼Œéœ€ç”¨æˆ·æ‰‹åŠ¨åŒ¹é… CRM å®¢æˆ·ï¼‰ |
| quotationTime | String (LocalDate) | æŠ¥ä»·æ—¥æœŸï¼Œæ ¼å¼ yyyy-MM-dd |
| validUntil | String (LocalDate) | æœ‰æ•ˆæœŸè‡³ï¼Œæ ¼å¼ yyyy-MM-dd |
| taxRate | BigDecimal | ç¨ŽçŽ‡ï¼ˆç™¾åˆ†æ¯”ï¼‰ï¼Œå¦‚ 13.0 |
| discountPercent | BigDecimal | æŠ˜æ‰£çŽ‡ï¼ˆç™¾åˆ†æ¯”ï¼‰ï¼Œå¦‚ 5.0 |
| remark | String | å¤‡æ³¨ |
| items | Array\<Item\> | ç‰©æ–™æ˜Žç»†åˆ—表 |
| items[].itemName | String | ç‰©æ–™åç§°ï¼ˆæ–‡æœ¬ï¼Œéœ€ç”¨æˆ·åœ¨ MdmItemSelect ä¸­åŒ¹é…ï¼‰ |
| items[].itemSpec | String | ç‰©æ–™è§„格型号 |
| items[].count | BigDecimal | æ•°é‡ |
| items[].quotationPrice | BigDecimal | å•ä»· |
| rawText | String | åŽŸå§‹è¯†åˆ«æ–‡æœ¬ï¼Œå‰ç«¯å¯é€‰å±•ç¤ºç”¨äºŽè°ƒè¯•/参考 |
所有字段均为可选,AI æœªè¯†åˆ«åˆ°çš„字段为 null。
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "name": "XX公司报价单",
    "customerName": "XX科技有限公司",
    "quotationTime": "2026-07-31",
    "validUntil": "2026-08-30",
    "taxRate": 13.0,
    "discountPercent": 5.0,
    "remark": "含税含运费",
    "items": [
      {
        "itemName": "伺服电机",
        "itemSpec": "SV-1000W",
        "count": 10,
        "quotationPrice": 1500.00
      }
    ],
    "rawText": "原始文件内容..."
  }
}
```
**错误情况(HTTP 200,所有字段为 null,rawText ä¸­åŒ…含错误提示):**
| rawText å€¼ | åœºæ™¯ |
|------------|------|
| æ–‡ä»¶ä¸å­˜åœ¨ | blobId æ— æ•ˆ |
| ä¸æ”¯æŒçš„æ–‡ä»¶ç±»åž‹ï¼Œè¯·ä¸Šä¼  PDF/Word/Excel/图片文件 | æ–‡ä»¶åŽç¼€ä¸åœ¨ç™½åå•内 |
| æ–‡ä»¶è¯»å–失败 | æ–‡ä»¶ä¸‹è½½å¤±è´¥ |
| æ–‡ä»¶å†…容为空,无法识别 | æ–‡ä»¶è§£æžå‡ºç©ºå†…容 |
| AI è¯†åˆ«æš‚时不可用,请手动录入 | AI æœåС异叏 |
## æ”¯æŒçš„æ–‡ä»¶ç±»åž‹
| ç±»åž‹ | åŽç¼€ |
|------|------|
| PDF | .pdf |
| Word | .doc, .docx |
| Excel | .xls, .xlsx |
| å›¾ç‰‡ | .png, .jpg, .jpeg, .gif, .bmp |
| çº¯æ–‡æœ¬ | .txt, .csv |
## æ•°æ®å¡«å…¥è§„则
| OCR å­—段 | å¡«å…¥æ–°å¢žè¡¨å•字段 | è¯´æ˜Ž |
|----------|-----------------|------|
| name | æŠ¥ä»·å•名称 | ç›´æŽ¥å¡«å…¥ |
| customerName | å®¢æˆ·åç§°ï¼ˆæ–‡æœ¬å±•示) | ä¸è‡ªåŠ¨åŒ¹é… CRM å®¢æˆ·ï¼Œç”¨æˆ·éœ€æ‰‹åŠ¨é€šè¿‡ CrmCustomerSelect é€‰æ‹© |
| quotationTime | æŠ¥ä»·æ—¥æœŸ | ç›´æŽ¥å¡«å…¥æ—¥æœŸé€‰æ‹©å™¨ |
| validUntil | æœ‰æ•ˆæœŸè‡³ | ç›´æŽ¥å¡«å…¥æ—¥æœŸé€‰æ‹©å™¨ |
| taxRate | ç¨Žçއ | ç›´æŽ¥å¡«å…¥ï¼Œå•位为百分比 |
| discountPercent | æŠ˜æ‰£çއ | ç›´æŽ¥å¡«å…¥ï¼Œå•位为百分比 |
| remark | å¤‡æ³¨ | ç›´æŽ¥å¡«å…¥ |
| items[].itemName + itemSpec | ç‰©æ–™æ˜Žç»†è¡Œ | å¡«å…¥ç‰©æ–™åç§°å’Œè§„格文本,用户需在 MdmItemSelect ä¸­æ‰‹åŠ¨åŒ¹é…å®žé™…ç‰©æ–™ |
| items[].count | ç‰©æ–™æ˜Žç»†è¡Œ - æ•°é‡ | ç›´æŽ¥å¡«å…¥ |
| items[].quotationPrice | ç‰©æ–™æ˜Žç»†è¡Œ - å•ä»· | ç›´æŽ¥å¡«å…¥ |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| AI è¯†åˆ«ç»“果全为 null | å¼¹çª—提示"未识别到报价数据",用户可手动录入或重新上传 |
| éƒ¨åˆ†å­—段识别成功 | é¢„填已有字段,缺失字段留空由用户手动填写 |
| è¯†åˆ«åˆ°å¤šä¸ªç‰©æ–™ä½†ç”¨æˆ·åªéœ€éƒ¨åˆ† | ç”¨æˆ·åœ¨æ–°å¢žå¼¹çª—中增删调整物料行 |
| å®¢æˆ·åç§°æ— æ³•匹配 CRM å®¢æˆ· | ç”¨æˆ·é€šè¿‡ CrmCustomerSelect æœç´¢å¹¶é€‰æ‹©æ­£ç¡®çš„客户 |
## æ³¨æ„äº‹é¡¹
- æ–‡ä»¶ä¸Šä¼ ä½¿ç”¨çŽ°æœ‰çš„ `/admin-api/system/storage-blob/upload` æŽ¥å£ï¼Œä¸è¦ä½¿ç”¨ `/infra/file/upload`
- æ–‡ä»¶å¤§å°é™åˆ¶ç”± system æ¨¡å—çš„ storage-blob ä¸Šä¼ æŽ¥å£æŽ§åˆ¶
- AI OCR è¯†åˆ«å¯èƒ½éœ€è¦å‡ ç§’时间,前端需显示 loading çŠ¶æ€
- `customerName` æ˜¯çº¯æ–‡æœ¬ï¼Œå‰ç«¯ä¸è¦å°è¯•自动匹配,需要用户通过 CrmCustomerSelect æ‰‹åŠ¨é€‰æ‹©
- `itemName` åŒç†ï¼Œç”¨æˆ·éœ€åœ¨ MdmItemSelect ä¸­æœç´¢ç‰©æ–™ç¼–码/名称来匹配实际物料
- è¯†åˆ«ç»“果中的日期格式为 `yyyy-MM-dd`,直接可用于日期选择器
- å½“ rawText ä¸­åŒ…含错误提示(如"文件不存在")时,前端应展示错误信息,不要继续填入表单流程
docs/mes_pro_ai_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
# ç”Ÿäº§æ¨¡å— AI è¾…助增强 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§å·¥å•详情页(MesProWorkOrder)
---
## åŠŸèƒ½ä¸€ï¼šAI ç‰©æ–™çŸ­ç¼ºé¢„测
### ä¸šåŠ¡æµç¨‹
```
在工单详情页
    â†’ ç‚¹å‡»"AI ç‰©æ–™é¢„测"按钮
    â†’ åŽç«¯æ”¶é›† BOM + åº“存数据,调用 AI
    â†’ å¼¹çª—展示物料风险分析(整体风险等级 + å„物料明细)
```
### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/pro/ai/predict-material` | AI ç‰©æ–™çŸ­ç¼ºé¢„测 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| workOrderId | Long | æ˜¯ | å·¥å• ID |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| riskLevel | Integer | æ•´ä½“风险等级:1=低风险 2=中风险 3=高风险 |
| reasoning | String | AI ç»¼åˆåˆ†æž |
| riskItems | List\<RiskItem\> | ç‰©æ–™é£Žé™©æ˜Žç»† |
**RiskItem å­å­—段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| itemCode | String | ç‰©æ–™ç¼–码 |
| itemName | String | ç‰©æ–™åç§° |
| requiredQuantity | BigDecimal | éœ€æ±‚量 |
| availableQuantity | BigDecimal | åº“存可用量 |
| shortageQuantity | BigDecimal | çŸ­ç¼ºæ•°é‡ |
| riskLevel | String | é£Žé™©ç­‰çº§ï¼šå……è¶³/ç´§å¼ /短缺 |
| suggestion | String | å»ºè®®æŽªæ–½ |
---
## åŠŸèƒ½äºŒï¼šAI ç”Ÿäº§æ—¶é•¿é¢„测
### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/pro/ai/predict-duration` | AI ç”Ÿäº§æ—¶é•¿é¢„测 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| predictedTotalHours | BigDecimal | é¢„测总时长(小时) |
| standardTotalHours | BigDecimal | æ ‡å‡†æ€»æ—¶é•¿ï¼ˆå°æ—¶ï¼‰ |
| reasoning | String | AI æŽ¨ç†åˆ†æž |
| keyPoints | List\<String\> | å…³é”®å…³æ³¨ç‚¹ |
| processBreakdown | List\<ProcessTimeBreakdown\> | å„工序时长明细 |
**ProcessTimeBreakdown å­å­—段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| processName | String | å·¥åºåç§° |
| sort | Integer | å·¥åºé¡ºåº |
| standardPrepareTime | Integer | æ ‡å‡†å‡†å¤‡æ—¶é—´ï¼ˆåˆ†é’Ÿï¼‰ |
| standardWaitTime | Integer | æ ‡å‡†ç­‰å¾…时间(分钟) |
| standardProductionTime | BigDecimal | æ ‡å‡†ç”Ÿäº§æ—¶é—´ï¼ˆå°æ—¶ï¼‰ |
| predictedProductionTime | BigDecimal | é¢„测生产时间(小时) |
---
## åŠŸèƒ½ä¸‰ï¼šAI ç”Ÿäº§é£Žé™©é¢„测
### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/pro/ai/predict-risk` | AI ç”Ÿäº§é£Žé™©é¢„测 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| overallRiskLevel | Integer | æ•´ä½“风险等级:1=低风险 2=中风险 3=高风险 |
| reasoning | String | AI ç»¼åˆåˆ†æž |
| keyPoints | List\<String\> | å…³é”®å…³æ³¨ç‚¹ |
| risks | List\<RiskItem\> | é£Žé™©æ˜Žç»†åˆ—表 |
**RiskItem å­å­—段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| category | String | é£Žé™©ç±»åˆ«ï¼šè´¨é‡/设备/物料/工艺瓶颈 |
| description | String | é£Žé™©æè¿° |
| severity | String | ä¸¥é‡ç¨‹åº¦ï¼šé«˜/中/低 |
| suggestion | String | å»ºè®®æŽªæ–½ |
---
## åŠŸèƒ½å››ï¼šAI æ˜¯å¦èƒ½æŒ‰æ—¶äº¤ä»˜
### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/pro/ai/predict-delivery` | AI æŒ‰æ—¶äº¤ä»˜é¢„测 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| onTime | Boolean | æ˜¯å¦èƒ½æŒ‰æ—¶äº¤ä»˜ |
| confidence | Double | ç½®ä¿¡åº¦ï¼ˆ0-100) |
| reasoning | String | AI æŽ¨ç†åˆ†æž |
| delayFactors | List\<String\> | å»¶æœŸå› ç´  |
| progressPercent | BigDecimal | ç”Ÿäº§è¿›åº¦ç™¾åˆ†æ¯” |
| remainingDays | Integer | è·ç¦»éœ€æ±‚日期剩余天数 |
---
## å­—段展示规则
### ç‰©æ–™é¢„测弹窗
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| riskLevel | å¼¹çª—顶部醒目显示(颜色:绿色=低风险、橙色=中风险、红色=高风险) | æ•´ä½“风险等级 |
| reasoning | å¼¹çª—正文区域 | æ®µè½æ–‡æœ¬ |
| riskItems | å¼¹çª—中部表格 | ç‰©æ–™æ˜Žç»†åˆ—表 |
### æ—¶é•¿é¢„测弹窗
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| predictedTotalHours | å¼¹çª—顶部醒目显示,对比 standardTotalHours | é¢„测 vs æ ‡å‡† |
| reasoning | å¼¹çª—正文区域 | æ®µè½æ–‡æœ¬ |
| keyPoints | å¼¹çª—底部 | icon + æ–‡æœ¬åˆ—表 |
| processBreakdown | å¼¹çª—中部表格 | æ¯é“工序的时长分解 |
### é£Žé™©é¢„测弹窗
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| overallRiskLevel | å¼¹çª—顶部醒目显示 | é¢œè‰²åŒºåˆ† |
| reasoning | å¼¹çª—正文区域 | æ®µè½æ–‡æœ¬ |
| keyPoints | å¼¹çª—中部 | å…³æ³¨ç‚¹åˆ—表 |
| risks | å¼¹çª—底部表格 | æŒ‰ç±»åˆ«åˆ†ç»„展示 |
### äº¤ä»˜é¢„测弹窗
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| onTime | å¼¹çª—顶部醒目显示(绿色=可交付、红色=不可交付) | æ˜¯å¦æŒ‰æ—¶ |
| confidence | onTime æ—è¾¹ | ç½®ä¿¡åº¦ç™¾åˆ†æ¯” |
| progressPercent | è¿›åº¦æ¡ | å¯è§†åŒ–进度 |
| remainingDays | è¿›åº¦æ¡å³ä¾§ | å‰©ä½™å¤©æ•° |
| reasoning | å¼¹çª—正文区域 | æ®µè½æ–‡æœ¬ |
| delayFactors | å¼¹çª—底部 | å»¶æœŸå› ç´ åˆ—表(仅不可交付时显示) |
---
## ä¸šåŠ¡è§„åˆ™
| åœºæ™¯ | è§„则 |
|------|------|
| ç‚¹å‡»æŒ‰é’® | æŒ‰é’®æ˜¾ç¤º loading çŠ¶æ€ï¼Œä¸å¯é‡å¤ç‚¹å‡» |
| AI è¿”回成功 | å¼¹çª—展示结果,仅作参考 |
| AI è¿”回失败 | å¼¹çª—显示"AI åˆ†æžæš‚时不可用,请稍后重试",不影响正常操作 |
| å·¥å•状态为草稿/已取消 | ä»å¯è°ƒç”¨ AI é¢„测(用于生产前评估) |
| æ—  BOM æ•°æ® | ç‰©æ–™é¢„测返回空风险项 |
| æœªæŽ’产(无任务) | æ—¶é•¿é¢„测仅返回标准时间估算 |
### æŒ‰é’®æ”¾ç½®ä½ç½®
在工单详情页的操作区域新增 4 ä¸ª AI è¾…助按钮。建议放在"确认/完成/取消"按钮区域的上方,用分隔线隔开:
```
[AI ç‰©æ–™é¢„测] [AI æ—¶é•¿é¢„测] [AI é£Žé™©é¢„测] [AI äº¤ä»˜è¯„ä¼°]
────────────────────────────────────
[确认工单] [完成工单] [取消工单]
```
4 ä¸ª AI æŒ‰é’®åœ¨å·¥å•状态为"草稿"和"已确认"时均可点击。
---
## æ³¨æ„äº‹é¡¹
- AI åˆ†æžå»ºè®®ä»…作参考,不自动修改工单数据
- AI å“åº”时间可能较长(5-15 ç§’),务必显示 loading çŠ¶æ€
- 4 ä¸ª AI ç«¯ç‚¹ç‹¬ç«‹è°ƒç”¨ï¼Œå‰ç«¯å¯æŒ‰éœ€è°ƒç”¨å…¶ä¸­ä¸€ä¸ªæˆ–多个
- `workOrderId` å–当前工单详情页的 `id` å­—段
- æ‰€æœ‰ç«¯ç‚¹æƒé™ä¸º `mes:pro-work-order:query`,当前用户需有生产工单查询权限
- å“åº”中的字段均为可选(AI è§£æžå¤±è´¥æ—¶å¯èƒ½ä¸ºç©ºï¼‰ï¼Œå‰ç«¯éœ€åšé˜²å¾¡æ€§åˆ¤æ–­
- ç‰©æ–™/时长/风险/交付 4 ä¸ªåˆ†æžå¯ç‹¬ç«‹ä½¿ç”¨ï¼Œæ— å…ˆåŽä¾èµ–
docs/mes_qc_ai_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
# MES è´¨æ£€ AI åŠŸèƒ½ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- æ¥æ–™æ£€éªŒå•详情页(IQC)
- è¿‡ç¨‹æ£€éªŒå•详情页(IPQC)
- å‡ºè´§æ£€éªŒå•详情页(OQC)
- é€€è´§æ£€éªŒå•详情页(RQC)
- è´¨æ£€æ ‡å‡†çŸ¥è¯†åº“管理页(可选)
---
## åŠŸèƒ½ä¸€ï¼šAI è¾…助判定检验结果
### ä¸šåŠ¡æµç¨‹
```
检验员录入指标检测值 + ç¼ºé™·è®°å½•
    â†’ ç‚¹å‡»"AI è¾…助判定"按钮
    â†’ è°ƒç”¨ AI æŽ¥å£èŽ·å–åˆ¤å®šå»ºè®®
    â†’ å¼¹çª—展示 AI å»ºè®®ï¼ˆåˆ¤å®šç»“æžœ + æŽ¨ç†ä¾æ® + å…³æ³¨ç‚¹ï¼‰
    â†’ æ£€éªŒå‘˜å‚考建议,手动选择最终判定结果
    â†’ ç‚¹å‡»"完成检验"
```
### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/mes/qc/ai/suggest-result` | AI è¾…助判定 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| qcId | Long | æ˜¯ | è´¨æ£€å• ID |
| qcType | Integer | æ˜¯ | è´¨æ£€ç±»åž‹ï¼š1=来料 2=过程 3=出货 4=退货 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| suggestedResult | Integer | å»ºè®®åˆ¤å®šç¼–码:1合格 2特采 3不合格退货 4不合格报废 |
| suggestedResultName | String | å»ºè®®åˆ¤å®šåç§° |
| reasoning | String | AI æŽ¨ç†ä¾æ®ï¼ˆæ–‡æœ¬ï¼‰ |
| keyPoints | List\<String\> | å…³é”®å…³æ³¨ç‚¹æ¸…单 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "suggestedResult": 1,
    "suggestedResultName": "合格",
    "reasoning": "所有检测指标均在允许范围内,缺陷数量未超过上限,建议判定为合格。",
    "keyPoints": ["外观检查指标接近上限,建议加强来料外观管控"]
  }
}
```
### å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| suggestedResultName | å¼¹çª—顶部醒目显示(用颜色区分:绿色=合格、橙色=特采、红色=不合格) | åˆ¤å®šå»ºè®® |
| reasoning | å¼¹çª—正文区域 | æŽ¨ç†ä¾æ®ï¼Œå±•示为段落文本 |
| keyPoints | å¼¹çª—底部,列表展示 | å…³æ³¨ç‚¹ä»¥ icon + æ–‡æœ¬å±•示 |
### ä¸šåŠ¡è§„åˆ™
| åœºæ™¯ | è§„则 |
|------|------|
| ç‚¹å‡»æŒ‰é’® | æŒ‰é’®æ˜¾ç¤º loading çŠ¶æ€ï¼Œ**不可重复点击** |
| AI è¿”回成功 | å¼¹çª—展示结果,检验员自行决定是否采纳 |
| AI è¿”回失败 | æç¤º"AI åˆ¤å®šæš‚时不可用,请人工判定",不影响正常检验流程 |
| AI åˆ¤å®šè¶…æ—¶ | è®¾ç½® 30 ç§’超时,超时后提示用户重试 |
| æœªå½•入检测值 | å…è®¸è°ƒç”¨ AI,AI ä¼šåŸºäºŽå·²æœ‰ç¼ºé™·è®°å½•给出建议 |
| å·²å®Œæˆæ£€éªŒ | ä¸éœ€è¦å±•示 AI åˆ¤å®šæŒ‰é’®ï¼ˆå·²å®ŒæˆçŠ¶æ€éšè—ï¼‰ |
### æŒ‰é’®æ”¾ç½®ä½ç½®
在检验单详情页的"完成检验"按钮旁边,新增 `AI è¾…助判定` æŒ‰é’®ã€‚按钮仅在质检单状态为"草稿"时展示。
---
## åŠŸèƒ½äºŒï¼šè´¨æ£€æ ‡å‡†çŸ¥è¯†åº“ RAG æ£€ç´¢
### ä¸šåŠ¡æµç¨‹
```
管理员:上传质检标准文档 â†’ ç³»ç»Ÿè‡ªåŠ¨åˆ†æ®µ+向量化
检验员:在质检页面右侧"质检助手"面板输入问题 â†’ æ£€ç´¢ç›¸å…³æ ‡å‡†æ®µè½ â†’ å±•示参考内容
```
### API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/admin-api/ai/knowledge/segment/search` | å‘量检索知识库分段 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| knowledgeId | Long | æ˜¯ | è´¨æ£€æ ‡å‡†çŸ¥è¯†åº“ ID |
| content | String | æ˜¯ | æ£€ç´¢æŸ¥è¯¢æ–‡æœ¬ï¼ˆè‡ªç„¶è¯­è¨€é—®é¢˜ï¼‰ |
| topK | Integer | å¦ | è¿”回数量,默认取知识库配置值(建议 3~5) |
| similarityThreshold | Double | å¦ | ç›¸ä¼¼åº¦é˜ˆå€¼ 0~1,建议 0.5~0.7 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | åˆ†æ®µ ID |
| content | String | åŒ¹é…çš„分段文本内容 |
| score | Double | ç›¸ä¼¼åº¦å¾—分 |
| documentName | String | æ¥æºæ–‡æ¡£åç§° |
| documentId | Long | æ¥æºæ–‡æ¡£ ID |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    {
      "id": 10,
      "content": "外观检验标准:表面应光滑平整,无裂纹、气泡、毛刺、锈蚀等缺陷...",
      "score": 0.85,
      "documentName": "来料检验作业指导书_v2.pdf",
      "documentId": 5
    }
  ]
}
```
### å‰ç½®å‡†å¤‡
1. åœ¨ AI çŸ¥è¯†åº“管理中创建名称为"质检标准知识库"的知识库
2. ä¸Šä¼ è´¨æ£€ç›¸å…³æ–‡æ¡£ï¼ˆå›½æ ‡ã€ä¼æ ‡ã€ä½œä¸šæŒ‡å¯¼ä¹¦ç­‰ï¼‰
3. è®°å½•知识库 ID,前端配置为常量或通过接口获取
### å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| content | ç»“果卡片正文 | é«˜äº®å‘½ä¸­å…³é”®è¯ |
| score | ç»“果卡片底部小字 | æ ¼å¼åŒ–为百分比,如"相似度 85%" |
| documentName | ç»“果卡片顶部 | ç‚¹å‡»å¯è·³è½¬åˆ°æ–‡æ¡£é¢„览页 |
---
## æ³¨æ„äº‹é¡¹
### AI åˆ¤å®š
- AI å»ºè®®ä»…作**参考**,最终判定由检验员决定,不自动写入
- AI å“åº”时间可能较长(5~15 ç§’),前端需显示 loading çŠ¶æ€
- `qcType` å–值:IQC è¯¦æƒ…页传 1、IPQC è¯¦æƒ…页传 2、OQC è¯¦æƒ…页传 3、RQC è¯¦æƒ…页传 4
- `qcId` å–当前质检单的 `id` å­—段
### çŸ¥è¯†åº“检索
- çŸ¥è¯†åº“ ID éœ€å…ˆåˆ›å»ºçŸ¥è¯†åº“并上传文档后才能使用
- å¦‚果检索无结果(空数组),展示"未找到相关标准,请尝试其他关键词"
- å»ºè®®åœ¨è´¨æ£€å½•入页面右侧用可折叠面板展示,不影响主操作区域
- æ£€ç´¢è¾“入框建议 placeholder:"输入问题,如:外观检验标准是什么?"
docs/oa_notice_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
# OA é€šçŸ¥å…¬å‘Š - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| é€šçŸ¥å…¬å‘Šåˆ—表 | `/bpm/oa/notice` | æ–°å»ºï¼š`src/views/bpm/oa/notice/index.vue` |
| é€šçŸ¥å…¬å‘Šè¯¦æƒ… | `/bpm/oa/notice/detail` | æ–°å»ºï¼š`src/views/bpm/oa/notice/detail.vue` |
## API
| æ–¹æ³• | è·¯å¾„ | æƒé™ | è¯´æ˜Ž |
|------|------|------|------|
| POST | `/bpm/oa/notice/create` | `bpm:oa-notice:create` | åˆ›å»º |
| PUT | `/bpm/oa/notice/update` | `bpm:oa-notice:update` | ä¿®æ”¹ |
| DELETE | `/bpm/oa/notice/delete` | `bpm:oa-notice:delete` | åˆ é™¤ |
| GET | `/bpm/oa/notice/page` | `bpm:oa-notice:query` | åˆ†é¡µåˆ—表 |
| GET | `/bpm/oa/notice/get` | `bpm:oa-notice:query` | è¯¦æƒ…(调用即标记已读) |
| GET | `/bpm/oa/notice/my-unread-count` | - | æˆ‘的未读数量 |
### è¯·æ±‚参数
**POST `/bpm/oa/notice/create` & PUT `/bpm/oa/notice/update`:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ï¼ˆæ›´æ–°æ—¶å¿…填) | é€šçŸ¥å…¬å‘ŠID |
| title | String | æ˜¯ | æ ‡é¢˜ï¼Œæœ€é•¿50字符 |
| content | String | æ˜¯ | å†…容 |
| type | Integer | æ˜¯ | ç±»åž‹ï¼š1=通知 2=公告 |
| status | Integer | æ˜¯ | çŠ¶æ€ï¼š0=草稿 1=已发布 2=已撤销 |
| sendScope | Integer | æ˜¯ | å‘送范围:1=通知所有人 2=指定人 |
| isTop | Integer | å¦ | æ˜¯å¦ç½®é¡¶ï¼š0=否 1=是 |
| userIds | List\<Long\> | å¦ï¼ˆsendScope=2时必填) | æŒ‡å®šç”¨æˆ·ID列表 |
**GET `/bpm/oa/notice/page`:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| title | String | å¦ | æ ‡é¢˜ï¼Œæ¨¡ç³ŠåŒ¹é… |
| type | Integer | å¦ | ç±»åž‹ |
| status | Integer | å¦ | çŠ¶æ€ |
| sendScope | Integer | å¦ | å‘送范围 |
| pageNo | Integer | å¦ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Integer | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
**GET `/bpm/oa/notice/get`:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é€šçŸ¥å…¬å‘ŠID |
**DELETE `/bpm/oa/notice/delete`:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é€šçŸ¥å…¬å‘ŠID |
### å“åº”格式
**分页列表 `GET /bpm/oa/notice/page` å“åº”:**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "title": "关于放假的通知",
        "content": "根据国家规定...",
        "type": 1,
        "status": 1,
        "sendScope": 1,
        "isTop": 0,
        "publishUserId": 1,
        "publishTime": "2025-01-01 10:00:00",
        "createTime": "2025-01-01 10:00:00"
      }
    ],
    "total": 100
  }
}
```
**详情 `GET /bpm/oa/notice/get` å“åº”:**
与列表项相同结构,额外返回:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| userIds | List\<Long\> | æŒ‡å®šç”¨æˆ·ID列表(仅 sendScope=2 æ—¶æœ‰å€¼ï¼‰ |
**我的未读数量 `GET /bpm/oa/notice/my-unread-count` å“åº”:**
```json
{
  "code": 0,
  "data": 5
}
```
## å­—段展示规则
| å­—段 | åˆ—表 | è¯¦æƒ… | è¯´æ˜Ž |
|------|------|------|------|
| title | âœ… | âœ… | é€šçŸ¥å…¬å‘Šæ ‡é¢˜ï¼Œç½®é¡¶é¡¹å‰åŠ  `[置顶]` æ ‡è¯† |
| type | âœ… | âœ… | 1=通知 2=公告,用 Tag åŒºåˆ†é¢œè‰² |
| status | âœ… | âœ… | 0=草稿(灰色)1=已发布(绿色)2=已撤销(红色)|
| sendScope | âœ… | âœ… | 1=通知所有人 2=指定人 |
| isTop | âœ… | âœ… | ç½®é¡¶é¡¹æŽ’在前面 |
| publishUserId | - | âœ… | å‘布人姓名(需关联用户表展示) |
| publishTime | âœ… | âœ… | å‘布时间 |
| createTime | âœ… | âœ… | åˆ›å»ºæ—¶é—´ |
| content | - | âœ… | å¯Œæ–‡æœ¬å†…容 |
| userIds | - | âœ… | ä»… sendScope=2 æ—¶å±•示指定人员列表 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| åˆ›å»ºæ—¶é€‰æ‹©ã€Œé€šçŸ¥æ‰€æœ‰äººã€ | æ‰€æœ‰ç™»å½•用户在列表页可见此公告 |
| åˆ›å»ºæ—¶é€‰æ‹©ã€ŒæŒ‡å®šäººã€ä¸”状态为「已发布」 | ä»…创建者+被指定用户可见;系统自动给指定人发送站内信 |
| åˆ›å»ºæ—¶çŠ¶æ€ä¸ºã€Œè‰ç¨¿ã€ | ä»…创建者本人可见,不发送通知 |
| ç‚¹å‡»è¯¦æƒ… | è‡ªåŠ¨æ ‡è®°å½“å‰ç”¨æˆ·ã€Œå·²è¯»ã€ï¼Œå†æ¬¡æ‰“å¼€ä¸é‡å¤è®°å½• |
| åˆ—表排序 | æŒ‰ isTop é™åºï¼Œå†æŒ‰ id é™åº |
| ä¿®æ”¹å…¬å‘Š | å¦‚果原来是草稿改为已发布,会触发发布时间更新和通知发送 |
| æ’¤é”€å…¬å‘Š | å°†çŠ¶æ€æ”¹ä¸ºã€Œå·²æ’¤é”€ã€åŽï¼Œåˆ—è¡¨ä»å¯è§ä½†æ˜¾ç¤ºä¸ºæ’¤é”€çŠ¶æ€ |
| é“ƒé“›é€šçŸ¥ | æŒ‡å®šäººæ¨¡å¼å‘布时发站内信(模板 code: `oa-notice-publish`),头部铃铛可显示未读数;通知所有人模式不逐条推送 |
## API å°è£…参考
新建 `src/api/bpm/oa/notice/index.ts`,参照 `src/api/bpm/oa/leave/index.ts` æ¨¡å¼ï¼š
```typescript
import { requestClient } from '#/api/request';
export namespace OaNoticeApi {
  export interface NoticeVO {
    id: number;
    title: string;
    content: string;
    type: number;
    status: number;
    sendScope: number;
    isTop: number;
    publishUserId: number;
    publishTime: string;
    createTime: string;
    userIds?: number[];
  }
  export interface NoticeSaveReqVO {
    id?: number;
    title: string;
    content: string;
    type: number;
    status: number;
    sendScope: number;
    isTop?: number;
    userIds?: number[];
  }
}
const BASE = '/bpm/oa/notice';
export function getNoticePage(params: any) {
  return requestClient.get(`${BASE}/page`, { params });
}
export function getNotice(id: number) {
  return requestClient.get(`${BASE}/get`, { params: { id } });
}
export function createNotice(data: OaNoticeApi.NoticeSaveReqVO) {
  return requestClient.post(`${BASE}/create`, data);
}
export function updateNotice(data: OaNoticeApi.NoticeSaveReqVO) {
  return requestClient.put(`${BASE}/update`, data);
}
export function deleteNotice(id: number) {
  return requestClient.delete(`${BASE}/delete`, { params: { id } });
}
export function getMyUnreadNoticeCount() {
  return requestClient.get(`${BASE}/my-unread-count`);
}
```
## é¡µé¢æ–‡ä»¶å»ºè®®
| æ–‡ä»¶ | è·¯å¾„ |
|------|------|
| åˆ—表页 | `src/views/bpm/oa/notice/index.vue` |
| è¯¦æƒ…页 | `src/views/bpm/oa/notice/detail.vue` |
| è¡¨å•弹窗 | `src/views/bpm/oa/notice/modules/form.vue` |
| åˆ—表配置 | `src/views/bpm/oa/notice/data.ts` |
## æ³¨æ„äº‹é¡¹
- åˆ—表页的可见数据由后端按 sendScope + å½“前用户自动过滤,前端无需额外处理
- åˆ›å»º/编辑表单中,「指定人」选择器仅在 sendScope=2 æ—¶æ˜¾ç¤º
- è¡¨å•提交时 status å¯ç›´æŽ¥é€‰æ‹©ã€Œå·²å‘布」,此时会立即触发通知发送
- å·²è¯»çŠ¶æ€ç”±åŽç«¯åœ¨è°ƒç”¨ get æŽ¥å£æ—¶è‡ªåŠ¨æ ‡è®°ï¼Œå‰ç«¯æ— éœ€è°ƒç”¨æ ‡è®°å·²è¯»æŽ¥å£
- å¤´éƒ¨é“ƒé“›çš„æœªè¯»æ•°å¦‚果能获取 `getUnreadNotifyMessageCount`,可以叠加 `getMyUnreadNoticeCount` çš„结果展示
- åŽç«¯èœå•需要配置 `component` å­—段为 `bpm/oa/notice/index`,权限标识分别为 `bpm:oa-notice:create/update/delete/query`
- å‚考现有页面:列表页参照 `src/views/system/notice/index.vue`,表单弹窗参照 `src/views/bpm/oa/leave/create.vue`
docs/test-quotation-files/ÄÏͨ¿Í»§±¨¼Ûµ¥.csv
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
报价单名称: å—通客户钢芯铝绞线采购报价
客户名称: å—通客户
报价日期: 2026-07-31
有效期至: 2026-08-30
税率: 13
折扣率: 5
备注: å«ç¨Žå«è¿è´¹ï¼Œ30天账期
序号,物料名称,规格型号,数量,单价(元)
1,铝杆,1A60,500,25.5
2,铝单线,L-1.72,300,18.2
3,钢芯铝绞线,JL/L1-40/10-6/1,2000,8.8
4,镀锌钢线,JG3A-1*1.5,800,6.5
5,铝合导体,LH8030JY-10,1500,15.3
docs/test-quotation-files/²âÊÔ¿Í»§±¨¼Ûµ¥.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
===========================================
          ç”µçº¿ç”µç¼†æŠ¥ä»·å•
===========================================
报价日期:2026年7月31日
有效期至:2026年9月15日
客户名称:测试
联系人电话:13598585685
-------------------------------------------
一、报价说明
-------------------------------------------
1. æœ¬æŠ¥ä»·å«13%增值税
2. æŠ¥ä»·æœ‰æ•ˆæœŸ45天
3. ä»˜æ¬¾æ–¹å¼ï¼šæœˆç»“30天
4. äº¤è´§æœŸï¼š10个工作日
-------------------------------------------
二、报价明细
-------------------------------------------
物料名称        è§„格型号        æ•°é‡      å•ä»·(元)
------------------------------------------------------
铝杆            1A60            1000千克  25.50
铝单线          L-1.72          500千克   18.20
钢芯铝绞线      JL/L1-40/10-6/1 3000千克  8.80
镀锌钢线        JG3A-1*1.5      1200千克  6.50
铝合导体        LH8030JY-10     800千米   15.30
-------------------------------------------
三、备注
-------------------------------------------
1. æ•´ä½“折扣率:5%
2. ä»¥ä¸Šä»·æ ¼å«ç¨Žå«è¿è´¹
3. è´¨ä¿æœŸ12个月
docs/test-quotation-files/ÁÄÌì¼Ç¼Éú³ÉÌáʾ´Ê.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
生成一张微信聊天截图,内容如下:
对方(客户沟通):
"李经理,这是这次要采购的清单,麻烦给个报价吧"
"一共5种"
"铝杆,规格1A60,要1000千克"
"铝单线,规格L-1.72,要500千克"
"钢芯铝绞线,规格JL/L1-40/10-6/1,要3000千克"
"镀锌钢线,规格JG3A-1*1.5,要1200千克"
"铝合导体,规格LH8030JY-10,要800千米"
自己(我方销售):
"收到张总,我整理一下明天给您出正式报价单"
"税率13%,整体给您打95折,含税含运费"
"有效期到9月15号"
pom.xml
@@ -42,7 +42,8 @@
        <module>yudao-module-aftersales-api</module>
        <module>yudao-module-aftersales</module>
        <!-- è¯·å‚考 https://doc.iocoder.cn/ai/build/ æ–‡æ¡£ï¼Œå®Œæˆ AI æ¨¡å—的启动!!! -->
<!--        <module>yudao-module-ai</module>-->
        <module>yudao-module-ai-api</module>
        <module>yudao-module-ai</module>
    </modules>
    <name>${project.artifactId}</name>
sql/mysql/oa_notice_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
-- =============================================
-- OA é€šçŸ¥å…¬å‘Šæ¨¡å—初始化
-- =============================================
CREATE TABLE IF NOT EXISTS oa_notice (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    title VARCHAR(50) NOT NULL COMMENT '公告标题',
    content TEXT NOT NULL COMMENT '公告内容',
    type TINYINT NOT NULL COMMENT '公告类型(1通知 2公告)',
    status TINYINT NOT NULL DEFAULT 0 COMMENT '状态(0草稿 1已发布 2已撤销)',
    send_scope TINYINT NOT NULL COMMENT '发送范围(1通知所有人 2指定人)',
    is_top TINYINT NOT NULL DEFAULT 0 COMMENT '是否置顶(0否 1是)',
    publish_user_id BIGINT COMMENT '发布人ID',
    publish_time DATETIME COMMENT '发布时间',
    creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted BIT DEFAULT 0 COMMENT '是否删除',
    INDEX idx_type (type),
    INDEX idx_status (status),
    INDEX idx_publish_time (publish_time),
    INDEX idx_is_top (is_top)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA通知公告表';
CREATE TABLE IF NOT EXISTS oa_notice_user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    notice_id BIGINT NOT NULL COMMENT '通知公告ID',
    user_id BIGINT NOT NULL COMMENT '指定用户ID',
    creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted BIT DEFAULT 0 COMMENT '是否删除',
    INDEX idx_notice_id (notice_id),
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA通知公告指定人关联表';
CREATE TABLE IF NOT EXISTS oa_notice_read (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    notice_id BIGINT NOT NULL COMMENT '通知公告ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    read_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '阅读时间',
    creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    deleted BIT DEFAULT 0 COMMENT '是否删除',
    UNIQUE KEY uk_notice_user (notice_id, user_id),
    INDEX idx_notice_id (notice_id),
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA通知公告已读记录表';
-- ç³»ç»Ÿé€šçŸ¥æ¨¡æ¿ï¼ˆç”¨äºŽOA通知公告发布时发送站内信)
INSERT INTO system_notify_template (name, code, nickname, content, type, params, status, creator, create_time, updater, update_time, deleted)
VALUES ('OA通知公告提醒', 'oa-notice-publish', '系统通知', '您有一条新的通知公告:{title}', 1, '["title"]', 0, '', NOW(), '', NOW(), 0)
ON DUPLICATE KEY UPDATE id = id;
yudao-module-aftersales/src/main/java/cn/iocoder/yudao/module/aftersales/controller/admin/ticket/vo/ticket/AfterSaleTicketSaveReqVO.java
@@ -38,9 +38,18 @@
    @Schema(description = "合同编号", example = "1")
    private Long contractId;
    @Schema(description = "问题类型: 1-一般问题 2-维修问题 3-退货问题", example = "1")
    private Integer issueType;
    @Schema(description = "问题分类", example = "外观缺陷")
    private String issueCategory;
    @Schema(description = "问题描述", example = "收到货后发现产品表面有划痕")
    private String issueDescription;
    @Schema(description = "严重程度: 0-轻微 1-一般 2-严重 3-致命", example = "1")
    private Integer severityLevel;
    @Schema(description = "备注", example = "客户要求尽快处理")
    private String remark;
yudao-module-ai-api/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-ai-api</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        ai æ¨¡å—下,AI èƒ½åŠ›çš„ API æŽ¥å£å®šä¹‰ï¼Œä¾›å…¶ä»–模块引用
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>
        <!-- Swagger æ³¨è§£ -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.ai.api.chat;
/**
 * AI å¯¹è¯ API,供其他模块调用
 */
public interface AiChatApi {
    /**
     * å‘送对话请求(纯文本)
     *
     * @param systemPrompt ç³»ç»Ÿæç¤ºè¯ï¼ˆè§’色设定)
     * @param userMessage  ç”¨æˆ·æ¶ˆæ¯
     * @return AI å›žå¤å†…容
     */
    String chat(String systemPrompt, String userMessage);
    /**
     * å‘送带图片的对话请求(多模态视觉识别)
     *
     * @param systemPrompt ç³»ç»Ÿæç¤ºè¯ï¼ˆè§’色设定)
     * @param userMessage  ç”¨æˆ·æ¶ˆæ¯
     * @param imageBase64  å›¾ç‰‡ base64 ç¼–码
     * @param mimeType     å›¾ç‰‡ MIME ç±»åž‹ï¼ˆå¦‚ image/png)
     * @return AI å›žå¤å†…容
     */
    String chatWithImage(String systemPrompt, String userMessage, String imageBase64, String mimeType);
}
yudao-module-ai/pom.xml
@@ -28,6 +28,11 @@
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-ai-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
            <version>${revision}</version>
        </dependency>
@@ -111,13 +116,6 @@
            <artifactId>spring-ai-starter-model-stability-ai</artifactId>
            <version>${spring-ai.version}</version>
        </dependency>
        <dependency>
            <!-- é€šä¹‰åƒé—® -->
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
            <version>${alibaba-ai.version}</version>
        </dependency>
        <!-- å‘量存储:https://db-engines.com/en/ranking/vector+dbms -->
        <dependency>
            <!-- Qdrant:https://qdrant.tech/ -->
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/api/chat/AiChatApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.ai.api.chat;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.content.Media;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.stereotype.Service;
import org.springframework.util.MimeTypeUtils;
import java.util.Base64;
import java.util.List;
@Service
@Slf4j
public class AiChatApiImpl implements AiChatApi {
    @Resource
    private AiModelService modelService;
    @Override
    public String chat(String systemPrompt, String userMessage) {
        AiModelDO defaultModel = modelService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType());
        ChatModel chatModel = modelService.getChatModel(defaultModel.getId());
        Prompt prompt = new Prompt(List.of(
                new SystemMessage(systemPrompt),
                new UserMessage(userMessage)));
        String result = chatModel.call(prompt).getResult().getOutput().getText();
        log.info("AI Chat è°ƒç”¨å®Œæˆï¼Œæ¨¡åž‹: {}, å›žå¤é•¿åº¦: {}", defaultModel.getModel(),
                result != null ? result.length() : 0);
        return result;
    }
    @Override
    public String chatWithImage(String systemPrompt, String userMessage, String imageBase64, String mimeType) {
        // ä¼˜å…ˆé€‰æ‹©è§†è§‰æ¨¡åž‹ï¼ˆæ¨¡åž‹åå« vl),否则回退到默认对话模型
        List<AiModelDO> chatModels = modelService.getModelListByStatusAndType(
                CommonStatusEnum.ENABLE.getStatus(), AiModelTypeEnum.CHAT.getType(), null);
        AiModelDO visionModel = chatModels.stream()
                .filter(m -> m.getModel() != null && m.getModel().contains("vl"))
                .findFirst()
                .orElseGet(() -> modelService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()));
        ChatModel chatModel = modelService.getChatModel(visionModel.getId());
        byte[] imageBytes = Base64.getDecoder().decode(imageBase64);
        Media media = new Media(MimeTypeUtils.parseMimeType(mimeType),
                new ByteArrayResource(imageBytes));
        UserMessage imageMessage = UserMessage.builder()
                .text(userMessage)
                .media(List.of(media))
                .build();
        Prompt prompt = new Prompt(List.of(
                new SystemMessage(systemPrompt),
                imageMessage));
        String result = chatModel.call(prompt).getResult().getOutput().getText();
        log.info("AI Chat(多模态) è°ƒç”¨å®Œæˆï¼Œæ¨¡åž‹: {}, å›žå¤é•¿åº¦: {}", visionModel.getModel(),
                result != null ? result.length() : 0);
        return result;
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java
@@ -1,36 +1,65 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - AI çŸ¥è¯†åº“")
@RestController
@RequestMapping("/ai/knowledge")
@Validated
public class AiKnowledgeController {
    @Resource
    private AiKnowledgeService knowledgeService;
    @Resource
    private AiKnowledgeDocumentService documentService;
    @PostMapping("/create")
    @Operation(summary = "创建知识库")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:create')")
    public CommonResult<Long> createKnowledge(@Valid @RequestBody AiKnowledgeSaveReqVO createReqVO) {
        return success(knowledgeService.createKnowledge(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新知识库")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateKnowledge(@Valid @RequestBody AiKnowledgeSaveReqVO updateReqVO) {
        knowledgeService.updateKnowledge(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除知识库")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
    public CommonResult<Boolean> deleteKnowledge(@RequestParam("id") Long id) {
        knowledgeService.deleteKnowledge(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获取知识库")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<AiKnowledgeRespVO> getKnowledge(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(knowledgeService.getKnowledge(id), AiKnowledgeRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获取知识库分页")
@@ -40,45 +69,11 @@
        return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
    }
    @GetMapping("/get")
    @Operation(summary = "获得知识库")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<AiKnowledgeRespVO> getKnowledge(@RequestParam("id") Long id) {
        AiKnowledgeDO knowledge = knowledgeService.getKnowledge(id);
        return success(BeanUtils.toBean(knowledge, AiKnowledgeRespVO.class));
    }
    @PostMapping("/create")
    @Operation(summary = "创建知识库")
    @PostMapping("/document/create")
    @Operation(summary = "创建文档")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:create')")
    public CommonResult<Long> createKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO createReqVO) {
        return success(knowledgeService.createKnowledge(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新知识库")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO updateReqVO) {
        knowledgeService.updateKnowledge(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除知识库")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
    public CommonResult<Boolean> deleteKnowledge(@RequestParam("id") Long id) {
        knowledgeService.deleteKnowledge(id);
        return success(true);
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得知识库的精简列表")
    public CommonResult<List<AiKnowledgeRespVO>> getKnowledgeSimpleList() {
        List<AiKnowledgeDO> list = knowledgeService.getKnowledgeSimpleListByStatus(CommonStatusEnum.ENABLE.getStatus());
        return success(convertList(list, knowledge -> new AiKnowledgeRespVO()
                .setId(knowledge.getId()).setName(knowledge.getName())));
    public CommonResult<List<Long>> createDocument(@Valid @RequestBody AiKnowledgeDocumentCreateReqVO createReqVO) {
        return success(documentService.createDocuments(createReqVO));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java
@@ -4,7 +4,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.*;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
import io.swagger.v3.oas.annotations.Operation;
@@ -12,7 +12,6 @@
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -22,69 +21,70 @@
@Tag(name = "管理后台 - AI çŸ¥è¯†åº“文档")
@RestController
@RequestMapping("/ai/knowledge/document")
@Validated
public class AiKnowledgeDocumentController {
    @Resource
    private AiKnowledgeDocumentService documentService;
    @GetMapping("/page")
    @Operation(summary = "获取文档分页")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPage(
            @Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
        PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
    }
    @GetMapping("/get")
    @Operation(summary = "获取文档详情")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<AiKnowledgeDocumentRespVO> getKnowledgeDocument(@RequestParam("id") Long id) {
        AiKnowledgeDocumentDO document = documentService.getKnowledgeDocument(id);
        return success(BeanUtils.toBean(document, AiKnowledgeDocumentRespVO.class));
    }
    @PostMapping("/create")
    @Operation(summary = "新建文档(单个)")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:create')")
    public CommonResult<Long> createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) {
        Long id = documentService.createKnowledgeDocument(reqVO);
        return success(id);
    }
    @PostMapping("/create-list")
    @Operation(summary = "新建文档(多个)")
    @Operation(summary = "创建文档(批量)")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:create')")
    public CommonResult<List<Long>> createKnowledgeDocumentList(
            @RequestBody @Valid AiKnowledgeDocumentCreateListReqVO reqVO) {
        List<Long> ids = documentService.createKnowledgeDocumentList(reqVO);
        return success(ids);
    public CommonResult<List<Long>> createDocumentList(@Valid @RequestBody AiKnowledgeDocumentCreateReqVO createReqVO) {
        return success(documentService.createDocuments(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新文档")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) {
        documentService.updateKnowledgeDocument(reqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新文档状态")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateKnowledgeDocumentStatus(
            @Valid @RequestBody AiKnowledgeDocumentUpdateStatusReqVO reqVO) {
        documentService.updateKnowledgeDocumentStatus(reqVO);
    public CommonResult<Boolean> updateDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO updateReqVO) {
        documentService.updateDocument(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除文档")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
    public CommonResult<Boolean> deleteKnowledgeDocument(@RequestParam("id") Long id) {
        documentService.deleteKnowledgeDocument(id);
    public CommonResult<Boolean> deleteDocument(@RequestParam("id") Long id) {
        documentService.deleteDocument(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获取文档")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<AiKnowledgeDocumentRespVO> getDocument(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(documentService.getDocument(id), AiKnowledgeDocumentRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获取文档分页")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
        PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getDocumentPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新文档状态")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateDocumentStatus(@Valid @RequestBody AiKnowledgeDocumentUpdateStatusReqVO updateStatusReqVO) {
        documentService.updateDocumentStatus(updateStatusReqVO);
        return success(true);
    }
    @PostMapping("/process/{id}")
    @Operation(summary = "处理文档(分段+向量化)")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> processDocument(@PathVariable("id") Long id) {
        documentService.processDocumentSegments(id);
        return success(true);
    }
    @GetMapping("/progress")
    @Operation(summary = "获取文档处理进度")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<List<AiKnowledgeSegmentProcessRespVO>> getProcessingProgress(@RequestParam("documentIds") List<Long> documentIds) {
        return success(documentService.getDocumentProcessingProgress(documentIds));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java
@@ -1,39 +1,30 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.*;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.hibernate.validator.constraints.URL;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - AI çŸ¥è¯†åº“段落")
@Tag(name = "管理后台 - AI çŸ¥è¯†åº“分段")
@RestController
@RequestMapping("/ai/knowledge/segment")
@Validated
public class AiKnowledgeSegmentController {
    @Resource
@@ -41,99 +32,105 @@
    @Resource
    private AiKnowledgeDocumentService documentService;
    @GetMapping("/get")
    @Operation(summary = "获取段落详情")
    @Parameter(name = "id", description = "段落编号", required = true, example = "1024")
    @GetMapping("/split")
    @Operation(summary = "预览文档切分结果")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<AiKnowledgeSegmentRespVO> getKnowledgeSegment(@RequestParam("id") Long id) {
        AiKnowledgeSegmentDO segment = segmentService.getKnowledgeSegment(id);
        return success(BeanUtils.toBean(segment, AiKnowledgeSegmentRespVO.class));
    public CommonResult<List<AiKnowledgeSegmentSplitRespVO>> previewSplit(@Valid AiKnowledgeSegmentSplitReqVO reqVO) {
        String fileUrl = reqVO.getUrl();
        if (fileUrl == null || fileUrl.isEmpty()) {
            throw new IllegalArgumentException("文档 URL ä¸èƒ½ä¸ºç©º");
        }
        // ä»Ž URL è·¯å¾„中提取文件名
        String name = fileUrl;
        int queryIdx = name.indexOf('?');
        if (queryIdx > 0) name = name.substring(0, queryIdx);
        int slashIdx = name.lastIndexOf('/');
        name = slashIdx >= 0 ? name.substring(slashIdx + 1) : name;
        List<String> segments = documentService.previewSplit(fileUrl, name, reqVO.getSegmentMaxTokens());
        List<AiKnowledgeSegmentSplitRespVO> result = new ArrayList<>();
        for (String content : segments) {
            result.add(new AiKnowledgeSegmentSplitRespVO()
                    .setContent(content)
                    .setContentLength(content.length())
                    .setTokens(estimateTokens(content)));
        }
        return success(result);
    }
    private int estimateTokens(String text) {
        if (text == null || text.isEmpty()) return 0;
        int chineseChars = 0, englishWords = 0;
        for (char c : text.toCharArray()) {
            if (c >= 0x4E00 && c <= 0x9FA5) chineseChars++;
        }
        for (String word : text.split("\\s+")) {
            if (word.matches(".*[a-zA-Z].*")) englishWords++;
        }
        return chineseChars + (int) (englishWords * 1.3);
    }
    @GetMapping("/get")
    @Operation(summary = "获取分段")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<AiKnowledgeSegmentRespVO> getSegment(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(segmentService.getSegment(id), AiKnowledgeSegmentRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获取段落分页")
    @Operation(summary = "获取分段分页")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(
            @Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
        PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
    public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
        PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getSegmentPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
    }
    @PostMapping("/create")
    @Operation(summary = "创建段落")
    @Operation(summary = "创建分段")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:create')")
    public CommonResult<Long> createKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO createReqVO) {
        return success(segmentService.createKnowledgeSegment(createReqVO));
    public CommonResult<Long> createSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO saveReqVO) {
        return success(segmentService.createSegment(saveReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新段落内容")
    @Operation(summary = "更新分段")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO reqVO) {
        segmentService.updateKnowledgeSegment(reqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "启禁用段落内容")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateKnowledgeSegmentStatus(
            @Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
        segmentService.updateKnowledgeSegmentStatus(reqVO);
    public CommonResult<Boolean> updateSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO saveReqVO) {
        segmentService.updateSegment(saveReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除段落")
    @Parameter(name = "id", description = "段落编号", required = true, example = "1024")
    @Operation(summary = "删除分段")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
    public CommonResult<Boolean> deleteKnowledgeSegment(@RequestParam("id") Long id) {
        segmentService.deleteKnowledgeSegment(id);
    public CommonResult<Boolean> deleteSegment(@RequestParam("id") Long id) {
        segmentService.deleteSegment(id);
        return success(true);
    }
    @GetMapping("/split")
    @Operation(summary = "切片内容")
    @Parameters({
            @Parameter(name = "url", description = "文档 URL", required = true),
            @Parameter(name = "segmentMaxTokens", description = "分段的最大 Token æ•°", required = true)
    })
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<List<AiKnowledgeSegmentRespVO>> splitContent(
            @RequestParam("url") @URL String url,
            @RequestParam(value = "segmentMaxTokens") Integer segmentMaxTokens) {
        List<AiKnowledgeSegmentDO> segments = segmentService.splitContent(url, segmentMaxTokens);
        return success(BeanUtils.toBean(segments, AiKnowledgeSegmentRespVO.class));
    @PutMapping("/update-status")
    @Operation(summary = "更新分段状态")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:update')")
    public CommonResult<Boolean> updateSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO updateStatusReqVO) {
        segmentService.updateSegmentStatus(updateStatusReqVO);
        return success(true);
    }
    @GetMapping("/get-process-list")
    @Operation(summary = "获取文档处理列表")
    @Parameter(name = "documentIds", description = "文档编号列表", required = true, example = "1,2,3")
    @Operation(summary = "获取文档处理进度")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<List<AiKnowledgeSegmentProcessRespVO>> getKnowledgeSegmentProcessList(
            @RequestParam("documentIds") List<Long> documentIds) {
        List<AiKnowledgeSegmentProcessRespVO> list = segmentService.getKnowledgeSegmentProcessList(documentIds);
        return success(list);
    public CommonResult<List<AiKnowledgeSegmentProcessRespVO>> getProcessList(@RequestParam("documentIds") List<Long> documentIds) {
        return success(documentService.getDocumentProcessingProgress(documentIds));
    }
    @GetMapping("/search")
    @Operation(summary = "搜索段落内容")
    @PostMapping("/search")
    @Operation(summary = "检索分段(RAG å‘量检索)")
    @PreAuthorize("@ss.hasPermission('ai:knowledge:query')")
    public CommonResult<List<AiKnowledgeSegmentSearchRespVO>> searchKnowledgeSegment(
            @Valid AiKnowledgeSegmentSearchReqVO reqVO) {
        // 1. æœç´¢æ®µè½
        List<AiKnowledgeSegmentSearchRespBO> segments = segmentService
                .searchKnowledgeSegment(BeanUtils.toBean(reqVO, AiKnowledgeSegmentSearchReqBO.class));
        if (CollUtil.isEmpty(segments)) {
            return success(Collections.emptyList());
        }
        // 2. æ‹¼æŽ¥ VO
        Map<Long, AiKnowledgeDocumentDO> documentMap = documentService.getKnowledgeDocumentMap(convertSet(
                segments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
        return success(BeanUtils.toBean(segments, AiKnowledgeSegmentSearchRespVO.class,
                segment -> MapUtils.findAndThen(documentMap, segment.getDocumentId(),
                        document -> segment.setDocumentName(document.getName()))));
    public CommonResult<List<AiKnowledgeSegmentSearchRespVO>> searchSegments(@Valid @RequestBody AiKnowledgeSegmentSearchReqVO searchReqVO) {
        AiKnowledgeSegmentSearchReqBO searchReqBO = BeanUtils.toBean(searchReqVO, AiKnowledgeSegmentSearchReqBO.class);
        List<AiKnowledgeSegmentSearchRespBO> results = segmentService.searchSegments(searchReqBO);
        return success(results.stream()
                .map(bo -> BeanUtils.toBean(bo, AiKnowledgeSegmentSearchRespVO.class))
                .collect(Collectors.toList()));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档创建 Request VO")
@Data
public class AiKnowledgeDocumentCreateReqVO {
    @Schema(description = "知识库编号", example = "1")
    @NotNull(message = "知识库不能为空")
    private Long knowledgeId;
    @Schema(description = "分段最大 Token æ•°", example = "500")
    private Integer segmentMaxTokens;
    @Schema(description = "文档列表")
    @NotEmpty(message = "文档列表不能为空")
    private List<@Valid DocumentItem> list;
    @Data
    public static class DocumentItem {
        @Schema(description = "文档名称", example = "产品手册.pdf")
        @NotEmpty(message = "文档名称不能为空")
        private String name;
        @Schema(description = "文档 URL ä¿¡æ¯")
        private UrlInfo url;
    }
    @Data
    public static class UrlInfo {
        @Schema(description = "文档 URL")
        private String url;
        @Schema(description = "文档名称")
        private String name;
        @Schema(description = "文件 ID")
        private Long id;
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java
@@ -3,15 +3,17 @@
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档的分页 Request VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AiKnowledgeDocumentPageReqVO extends PageParam {
    @Schema(description = "知识库编号", example = "1")
    private Long knowledgeId;
    @Schema(description = "文档名称", example = "Java å¼€å‘手册")
    @Schema(description = "文档名称")
    private String name;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java
@@ -2,44 +2,40 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档 Response VO")
@Data
public class AiKnowledgeDocumentRespVO {
    @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    @Schema(description = "知识库编号", example = "1")
    private Long knowledgeId;
    @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java å¼€å‘手册")
    @Schema(description = "文档名称", example = "产品手册")
    private String name;
    @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn")
    @Schema(description = "文档 URL")
    private String url;
    @Schema(description = "文档内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java æ˜¯ä¸€é—¨é¢å‘对象的语言.....")
    private String content;
    @Schema(description = "文档内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
    @Schema(description = "内容长度")
    private Integer contentLength;
    @Schema(description = "文档 Token æ•°é‡", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @Schema(description = "Token æ•°")
    private Integer tokens;
    @Schema(description = "分片最大 Token æ•°", requiredMode = Schema.RequiredMode.REQUIRED, example = "512")
    @Schema(description = "分段最大 Token æ•°")
    private Integer segmentMaxTokens;
    @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @Schema(description = "检索次数")
    private Integer retrievalCount;
    @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java
@@ -8,14 +8,14 @@
@Data
public class AiKnowledgeDocumentUpdateReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
    @Schema(description = "编号", example = "1")
    @NotNull(message = "编号不能为空")
    private Long id;
    @Schema(description = "名称", example = "Java å¼€å‘手册")
    @Schema(description = "文档名称", example = "产品手册")
    private String name;
    @Schema(description = "分片最大 Token æ•°", example = "1000")
    private Integer segmentMaxTokens;
    @Schema(description = "文档 URL")
    private String url;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java
@@ -1,7 +1,5 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -10,13 +8,12 @@
@Data
public class AiKnowledgeDocumentUpdateStatusReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
    @Schema(description = "编号", example = "1")
    @NotNull(message = "编号不能为空")
    private Long id;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @Schema(description = "状态", example = "0")
    @NotNull(message = "状态不能为空")
    @InEnum(CommonStatusEnum.class)
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java
@@ -1,25 +1,22 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“的分页 Request VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AiKnowledgePageReqVO extends PageParam {
    @Schema(description = "知识库名称", example = "芋艿")
    @Schema(description = "知识库名称")
    private String name;
    @Schema(description = "是否启用", example = "1")
    @InEnum(CommonStatusEnum.class)
    @Schema(description = "状态")
    private Integer status;
    @Schema(description = "创建时间")
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java
@@ -2,38 +2,37 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“ Response VO")
@Data
public class AiKnowledgeRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro ç”¨æˆ·æŒ‡å—")
    @Schema(description = "知识库名称", example = "产品文档")
    private String name;
    @Schema(description = "知识库描述", example = "帮助你快速构建系统")
    @Schema(description = "知识库描述", example = "产品相关文档")
    private String description;
    @Schema(description = "向量模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14")
    @Schema(description = "向量模型编号", example = "1")
    private Long embeddingModelId;
    @Schema(description = "向量模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat")
    @Schema(description = "向量模型标识", example = "text-embedding-v3")
    private String embeddingModel;
    @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
    @Schema(description = "TopK", example = "3")
    private Integer topK;
    @Schema(description = "相似度阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.7")
    @Schema(description = "相似度阈值", example = "0.7")
    private Double similarityThreshold;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java
@@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -11,31 +9,27 @@
@Data
public class AiKnowledgeSaveReqVO {
    @Schema(description = "对话编号", example = "1204")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro ç”¨æˆ·æŒ‡å—")
    @NotBlank(message = "知识库名称不能为空")
    @Schema(description = "知识库名称", example = "产品文档")
    @NotEmpty(message = "知识库名称不能为空")
    private String name;
    @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro æ“ä½œæ–‡æ¡£")
    @Schema(description = "知识库描述", example = "产品相关文档")
    private String description;
    @Schema(description = "向量模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "向量模型编号", example = "1")
    @NotNull(message = "向量模型不能为空")
    private Long embeddingModelId;
    @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
    @NotNull(message = "topK ä¸èƒ½ä¸ºç©º")
    @Schema(description = "TopK", example = "3")
    private Integer topK;
    @Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5")
    @NotNull(message = "相似性阈值不能为空")
    @Schema(description = "相似度阈值", example = "0.7")
    private Double similarityThreshold;
    @Schema(description = "是否启用",  requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "是否启用不能为空")
    @InEnum(CommonStatusEnum.class)
    @Schema(description = "状态", example = "0")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java
@@ -1,23 +1,22 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段的分页 Request VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AiKnowledgeSegmentPageReqVO extends PageParam {
    @Schema(description = "文档编号", example = "1")
    private Long documentId;
    @Schema(description = "分段内容关键字", example = "Java å¼€å‘")
    @Schema(description = "分段内容")
    private String content;
    @Schema(description = "分段状态", example = "1")
    @InEnum(CommonStatusEnum.class)
    @Schema(description = "状态")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java
@@ -3,17 +3,17 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“段落向量进度 Response VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段处理进度 Response VO")
@Data
public class AiKnowledgeSegmentProcessRespVO {
    @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "文档编号", example = "1")
    private Long documentId;
    @Schema(description = "总段落数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @Schema(description = "分段总数")
    private Long count;
    @Schema(description = "已向量化段落数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
    @Schema(description = "已向量化数量")
    private Long embeddingCount;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java
@@ -2,39 +2,40 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档分片 Response VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段 Response VO")
@Data
public class AiKnowledgeSegmentRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    private Long documentId;
    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    @Schema(description = "知识库编号", example = "1")
    private Long knowledgeId;
    @Schema(description = "向量库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1858496a-1dde-4edf-a43e-0aed08f37f8c")
    private String vectorId;
    @Schema(description = "文档编号", example = "1")
    private Long documentId;
    @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java å¼€å‘手册")
    @Schema(description = "分段内容")
    private String content;
    @Schema(description = "切片内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @Schema(description = "内容长度")
    private Integer contentLength;
    @Schema(description = "token æ•°é‡", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @Schema(description = "向量 ID")
    private String vectorId;
    @Schema(description = "Token æ•°")
    private Integer tokens;
    @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @Schema(description = "检索次数")
    private Integer retrievalCount;
    @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long createTime;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java
@@ -2,20 +2,27 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI æ–°å¢ž/修改知识库段落 request VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段新增/修改 Request VO")
@Data
public class AiKnowledgeSegmentSaveReqVO {
    @Schema(description = "编号", example = "24790")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "知识库文档编号", example = "1024")
    @Schema(description = "知识库编号(创建时必填,更新时可从已有分段获取)", example = "1")
    private Long knowledgeId;
    @Schema(description = "文档编号(创建时必填,更新时可从已有分段获取)", example = "1")
    private Long documentId;
    @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java å¼€å‘手册")
    @NotEmpty(message = "切片内容不能为空")
    @Schema(description = "分段内容")
    @NotEmpty(message = "分段内容不能为空")
    private String content;
    @Schema(description = "状态", example = "0")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java
@@ -1,24 +1,23 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“段落搜索 Request VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段检索 Request VO")
@Data
public class AiKnowledgeSegmentSearchReqVO {
    @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @Schema(description = "知识库编号", example = "1")
    @NotNull(message = "知识库编号不能为空")
    private Long knowledgeId;
    @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "如何使用这个产品")
    @NotEmpty(message = "内容不能为空")
    @Schema(description = "检索内容")
    @NotEmpty(message = "检索内容不能为空")
    private String content;
    @Schema(description = "最大返回数量", example = "5")
    @Schema(description = "返回数量", example = "3")
    private Integer topK;
    @Schema(description = "相似度阈值", example = "0.7")
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java
@@ -3,14 +3,32 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“段落搜索 Response VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段检索 Response VO")
@Data
public class AiKnowledgeSegmentSearchRespVO extends AiKnowledgeSegmentRespVO {
public class AiKnowledgeSegmentSearchRespVO {
    @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品使用手册")
    @Schema(description = "分段编号", example = "1")
    private Long id;
    @Schema(description = "文档编号", example = "1")
    private Long documentId;
    @Schema(description = "文档名称", example = "xxx.pdf")
    private String documentName;
    @Schema(description = "相似度分数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.95")
    @Schema(description = "知识库编号", example = "1")
    private Long knowledgeId;
    @Schema(description = "分段内容")
    private String content;
    @Schema(description = "内容长度")
    private Integer contentLength;
    @Schema(description = "Token æ•°")
    private Integer tokens;
    @Schema(description = "相似度分数", example = "0.85")
    private Double score;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSplitReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档切分预览 Request VO")
@Data
public class AiKnowledgeSegmentSplitReqVO {
    @Schema(description = "分段最大 Token æ•°", example = "500")
    private Integer segmentMaxTokens;
    @Schema(description = "文档下载 URL(完整的文件地址)")
    private String url;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSplitRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“文档切分预览 Response VO")
@Data
public class AiKnowledgeSegmentSplitRespVO {
    @Schema(description = "分段文本")
    private String content;
    @Schema(description = "内容长度")
    private Integer contentLength;
    @Schema(description = "估算 Token æ•°")
    private Integer tokens;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateStatusReqVO.java
@@ -1,22 +1,19 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“段落的更新状态 Request VO")
@Schema(description = "管理后台 - AI çŸ¥è¯†åº“分段更新状态 Request VO")
@Data
public class AiKnowledgeSegmentUpdateStatusReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
    @Schema(description = "编号", example = "1")
    @NotNull(message = "编号不能为空")
    private Long id;
    @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "是否启用不能为空")
    @InEnum(CommonStatusEnum.class)
    @Schema(description = "状态", example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java
@@ -6,27 +6,20 @@
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelRespVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@Tag(name = "管理后台 - AI API å¯†é’¥")
@RestController
@RequestMapping("/ai/api-key")
@Validated
public class AiApiKeyController {
    @Resource
@@ -49,7 +42,6 @@
    @DeleteMapping("/delete")
    @Operation(summary = "删除 API å¯†é’¥")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('ai:api-key:delete')")
    public CommonResult<Boolean> deleteApiKey(@RequestParam("id") Long id) {
        apiKeyService.deleteApiKey(id);
@@ -57,27 +49,18 @@
    }
    @GetMapping("/get")
    @Operation(summary = "获得 API å¯†é’¥")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @Operation(summary = "获取 API å¯†é’¥")
    @PreAuthorize("@ss.hasPermission('ai:api-key:query')")
    public CommonResult<AiApiKeyRespVO> getApiKey(@RequestParam("id") Long id) {
        AiApiKeyDO apiKey = apiKeyService.getApiKey(id);
        return success(BeanUtils.toBean(apiKey, AiApiKeyRespVO.class));
        return success(BeanUtils.toBean(apiKeyService.getApiKey(id), AiApiKeyRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得 API å¯†é’¥åˆ†é¡µ")
    @Operation(summary = "获取 API å¯†é’¥åˆ†é¡µ")
    @PreAuthorize("@ss.hasPermission('ai:api-key:query')")
    public CommonResult<PageResult<AiApiKeyRespVO>> getApiKeyPage(@Valid AiApiKeyPageReqVO pageReqVO) {
        PageResult<AiApiKeyDO> pageResult = apiKeyService.getApiKeyPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, AiApiKeyRespVO.class));
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得 API å¯†é’¥åˆ†é¡µåˆ—表")
    public CommonResult<List<AiModelRespVO>> getApiKeySimpleList() {
        List<AiApiKeyDO> list = apiKeyService.getApiKeyList();
        return success(convertList(list, key -> new AiModelRespVO().setId(key.getId()).setName(key.getName())));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java
@@ -33,14 +33,14 @@
    private AiModelService modelService;
    @PostMapping("/create")
    @Operation(summary = "创建模型")
    @Operation(summary = "创建 AI æ¨¡åž‹")
    @PreAuthorize("@ss.hasPermission('ai:model:create')")
    public CommonResult<Long> createModel(@Valid @RequestBody AiModelSaveReqVO createReqVO) {
        return success(modelService.createModel(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新模型")
    @Operation(summary = "更新 AI æ¨¡åž‹")
    @PreAuthorize("@ss.hasPermission('ai:model:update')")
    public CommonResult<Boolean> updateModel(@Valid @RequestBody AiModelSaveReqVO updateReqVO) {
        modelService.updateModel(updateReqVO);
@@ -48,7 +48,7 @@
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除模型")
    @Operation(summary = "删除 AI æ¨¡åž‹")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('ai:model:delete')")
    public CommonResult<Boolean> deleteModel(@RequestParam("id") Long id) {
@@ -57,16 +57,15 @@
    }
    @GetMapping("/get")
    @Operation(summary = "获得模型")
    @Operation(summary = "获取 AI æ¨¡åž‹")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('ai:model:query')")
    public CommonResult<AiModelRespVO> getModel(@RequestParam("id") Long id) {
        AiModelDO model = modelService.getModel(id);
        return success(BeanUtils.toBean(model, AiModelRespVO.class));
        return success(BeanUtils.toBean(modelService.getModel(id), AiModelRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得模型分页")
    @Operation(summary = "获取 AI æ¨¡åž‹åˆ†é¡µ")
    @PreAuthorize("@ss.hasPermission('ai:model:query')")
    public CommonResult<PageResult<AiModelRespVO>> getModelPage(@Valid AiModelPageReqVO pageReqVO) {
        PageResult<AiModelDO> pageResult = modelService.getModelPage(pageReqVO);
@@ -74,16 +73,19 @@
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得模型列表")
    @Parameter(name = "type", description = "类型", required = true, example = "1")
    @Parameter(name = "platform", description = "平台", example = "midjourney")
    @Operation(summary = "获得模型简单列表", description = "用于下拉选择器等场景")
    @Parameter(name = "type", description = "模型类型", required = true, example = "1")
    @Parameter(name = "platform", description = "模型平台", example = "TongYi")
    public CommonResult<List<AiModelRespVO>> getModelSimpleList(
            @RequestParam("type") Integer type,
            @RequestParam(value = "platform", required = false) String platform) {
        List<AiModelDO> list = modelService.getModelListByStatusAndType(
                CommonStatusEnum.ENABLE.getStatus(), type, platform);
        return success(convertList(list, model -> new AiModelRespVO().setId(model.getId())
                .setName(model.getName()).setModel(model.getModel()).setPlatform(model.getPlatform())));
        return success(convertList(list, model -> new AiModelRespVO()
                .setId(model.getId())
                .setName(model.getName())
                .setModel(model.getModel())
                .setPlatform(model.getPlatform())));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java
@@ -1,25 +1,22 @@
package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey;
import lombok.*;
import java.util.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - AI API å¯†é’¥åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AiApiKeyPageReqVO extends PageParam {
    @Schema(description = "名称", example = "文心一言")
    @Schema(description = "密钥名称")
    private String name;
    @Schema(description = "平台", example = "OpenAI")
    @Schema(description = "平台")
    private String platform;
    @Schema(description = "状态", example = "1")
    @Schema(description = "状态")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java
@@ -1,28 +1,29 @@
package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI API å¯†é’¥ Response VO")
@Data
public class AiApiKeyRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23538")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "文心一言")
    @Schema(description = "密钥名称", example = "通义千问")
    private String name;
    @Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
    private String apiKey;
    @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
    @Schema(description = "平台", example = "TongYi")
    private String platform;
    @Schema(description = "自定义 API åœ°å€", example = "https://aip.baidubce.com")
    @Schema(description = "API åœ°å€")
    private String url;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java
@@ -1,34 +1,32 @@
package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.*;
import jakarta.validation.constraints.*;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Schema(description = "管理后台 - AI API å¯†é’¥æ–°å¢ž/修改 Request VO")
@Data
public class AiApiKeySaveReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23538")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "文心一言")
    @NotEmpty(message = "名称不能为空")
    @Schema(description = "密钥名称", example = "通义千问")
    @NotEmpty(message = "密钥名称不能为空")
    private String name;
    @Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
    @NotEmpty(message = "密钥不能为空")
    @Schema(description = "API å¯†é’¥", example = "sk-xxx")
    @NotEmpty(message = "API å¯†é’¥ä¸èƒ½ä¸ºç©º")
    private String apiKey;
    @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
    @Schema(description = "平台", example = "TongYi")
    @NotEmpty(message = "平台不能为空")
    private String platform;
    @Schema(description = "自定义 API åœ°å€", example = "https://aip.baidubce.com")
    @Schema(description = "API åœ°å€")
    private String url;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "状态不能为空")
    @Schema(description = "状态", example = "0")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java
@@ -1,20 +1,25 @@
package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model;
import lombok.*;
import io.swagger.v3.oas.annotations.media.Schema;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - API æ¨¡åž‹åˆ†é¡µ Request VO")
@Schema(description = "管理后台 - AI æ¨¡åž‹åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class AiModelPageReqVO extends PageParam {
    @Schema(description = "模型名字", example = "张三")
    @Schema(description = "模型名称")
    private String name;
    @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125")
    private String model;
    @Schema(description = "模型类型")
    private Integer type;
    @Schema(description = "模型平台", example = "OpenAI")
    @Schema(description = "平台")
    private String platform;
    @Schema(description = "状态")
    private Integer status;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java
@@ -2,47 +2,48 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - AI æ¨¡åž‹ Response VO")
@Data
@Accessors(chain = true)
public class AiModelRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2630")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "API ç§˜é’¥ç¼–号", example = "22042")
    @Schema(description = "API å¯†é’¥ç¼–号", example = "1")
    private Long keyId;
    @Schema(description = "模型名字", example = "张三")
    @Schema(description = "模型名字", example = "通义千问")
    private String name;
    @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125")
    @Schema(description = "模型标识", example = "qwen-plus")
    private String model;
    @Schema(description = "模型平台", example = "OpenAI")
    @Schema(description = "模型平台", example = "TongYi")
    private String platform;
    @Schema(description = "模型类型", example = "1")
    private Integer type;
    @Schema(description = "排序", example = "1")
    @Schema(description = "排序", example = "0")
    private Integer sort;
    @Schema(description = "状态", example = "2")
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "温度参数", example = "1")
    @Schema(description = "温度")
    private Double temperature;
    @Schema(description = "单条回复的最大 Token æ•°é‡", example = "4096")
    @Schema(description = "最大 Token æ•°")
    private Integer maxTokens;
    @Schema(description = "上下文的最大 Message æ•°é‡", example = "8192")
    @Schema(description = "最大上下文数")
    private Integer maxContexts;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java
@@ -1,59 +1,50 @@
package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - API æ¨¡åž‹æ–°å¢ž/修改 Request VO")
@Schema(description = "管理后台 - AI æ¨¡åž‹æ–°å¢ž/修改 Request VO")
@Data
public class AiModelSaveReqVO {
    @Schema(description = "编号", example = "2630")
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "API ç§˜é’¥ç¼–号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22042")
    @NotNull(message = "API ç§˜é’¥ç¼–号不能为空")
    @Schema(description = "API å¯†é’¥ç¼–号", example = "1")
    @NotNull(message = "API å¯†é’¥ä¸èƒ½ä¸ºç©º")
    private Long keyId;
    @Schema(description = "模型名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
    @Schema(description = "模型名字", example = "通义千问")
    @NotEmpty(message = "模型名字不能为空")
    private String name;
    @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
    @Schema(description = "模型标识", example = "qwen-plus")
    @NotEmpty(message = "模型标识不能为空")
    private String model;
    @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
    @Schema(description = "模型平台", example = "TongYi")
    @NotEmpty(message = "模型平台不能为空")
    @InEnum(AiPlatformEnum.class)
    private String platform;
    @Schema(description = "模型类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @Schema(description = "模型类型", example = "1")
    @NotNull(message = "模型类型不能为空")
    @InEnum(AiModelTypeEnum.class)
    private Integer type;
    @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "排序不能为空")
    @Schema(description = "排序", example = "0")
    private Integer sort;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @InEnum(CommonStatusEnum.class)
    @NotNull(message = "状态不能为空")
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "温度参数", example = "1")
    @Schema(description = "温度")
    private Double temperature;
    @Schema(description = "单条回复的最大 Token æ•°é‡", example = "4096")
    @Schema(description = "最大 Token æ•°")
    private Integer maxTokens;
    @Schema(description = "上下文的最大 Message æ•°é‡", example = "8192")
    @Schema(description = "最大上下文数")
    private Integer maxContexts;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.http
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowPageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowSaveReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java
@@ -14,50 +14,33 @@
 * @author xiaoxin
 */
@TableName(value = "ai_knowledge", autoResultMap = true)
@KeySequence("ai_knowledge_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@KeySequence("ai_knowledge_seq")
@Data
public class AiKnowledgeDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * çŸ¥è¯†åº“名称
     */
    private String name;
    /**
     * çŸ¥è¯†åº“描述
     */
    private String description;
    /**
     * å‘量模型编号
     *
     * å…³è” {@link AiModelDO#getId()}
     * å‘量模型编号,关联 {@link AiModelDO#getId()}
     */
    private Long embeddingModelId;
    /**
     * æ¨¡åž‹æ ‡è¯†
     *
     * å†—ä½™ {@link AiModelDO#getModel()}
     * æ¨¡åž‹æ ‡è¯†ï¼Œå†—ä½™ {@link AiModelDO#getModel()}
     */
    private String embeddingModel;
    /**
     * topK
     */
    private Integer topK;
    /**
     * ç›¸ä¼¼åº¦é˜ˆå€¼
     */
    private Double similarityThreshold;
    /**
     * çŠ¶æ€
     * <p>
     * æžšä¸¾ {@link CommonStatusEnum}
     * çŠ¶æ€ï¼Œæžšä¸¾ {@link CommonStatusEnum}
     */
    private Integer status;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java
@@ -13,56 +13,31 @@
 * @author xiaoxin
 */
@TableName(value = "ai_knowledge_document")
@KeySequence("ai_knowledge_document_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@KeySequence("ai_knowledge_document_seq")
@Data
public class AiKnowledgeDocumentDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * çŸ¥è¯†åº“编号
     * <p>
     * å…³è” {@link AiKnowledgeDO#getId()}
     */
    private Long knowledgeId;
    /**
     * æ–‡æ¡£åç§°
     */
    private String name;
    /**
     * æ–‡ä»¶ URL
     */
    private String url;
    /**
     * å†…容
     */
    private String content;
    /**
     * æ–‡æ¡£é•¿åº¦
     */
    private Integer contentLength;
    /**
     * æ–‡æ¡£ token æ•°é‡
     */
    private Integer tokens;
    /**
     * åˆ†ç‰‡æœ€å¤§ Token æ•°
     */
    private Integer segmentMaxTokens;
    /**
     * å¬å›žæ¬¡æ•°
     */
    private Integer retrievalCount;
    /**
     * çŠ¶æ€
     * <p>
     * æžšä¸¾ {@link CommonStatusEnum}
     * çŠ¶æ€ï¼Œæžšä¸¾ {@link CommonStatusEnum}
     */
    private Integer status;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java
@@ -13,59 +13,31 @@
 * @author xiaoxin
 */
@TableName(value = "ai_knowledge_segment")
@KeySequence("ai_knowledge_segment_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@KeySequence("ai_knowledge_segment_seq")
@Data
public class AiKnowledgeSegmentDO extends BaseDO {
    /**
     * å‘量库的编号 - ç©ºå€¼
     */
    public static final String VECTOR_ID_EMPTY = "";
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * çŸ¥è¯†åº“编号
     * <p>
     * å…³è” {@link AiKnowledgeDO#getId()}
     */
    private Long knowledgeId;
    /**
     * æ–‡æ¡£ç¼–号
     * <p>
     * å…³è” {@link AiKnowledgeDocumentDO#getId()}
     */
    private Long documentId;
    /**
     * åˆ‡ç‰‡å†…容
     */
    private String content;
    /**
     * åˆ‡ç‰‡å†…容长度
     */
    private Integer contentLength;
    /**
     * å‘量库的编号
     */
    private String vectorId;
    /**
     * token æ•°é‡
     */
    private Integer tokens;
    /**
     * å¬å›žæ¬¡æ•°
     */
    private Integer retrievalCount;
    /**
     * çŠ¶æ€
     * <p>
     * æžšä¸¾ {@link CommonStatusEnum}
     * çŠ¶æ€ï¼Œæžšä¸¾ {@link CommonStatusEnum}
     */
    private Integer status;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java
@@ -1,53 +1,42 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
 * AI API ç§˜é’¥ DO
 * AI API å¯†é’¥ DO
 *
 * @author èŠ‹é“æºç 
 * @author fansili
 */
@TableName("ai_api_key")
@KeySequence("ai_api_key_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@KeySequence("ai_api_key_seq")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiApiKeyDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * åç§°
     */
    private String name;
    /**
     * å¯†é’¥
     */
    private String apiKey;
    /**
     * å¹³å°
     *
     * æžšä¸¾ {@link AiPlatformEnum}
     * å¹³å°ï¼Œæžšä¸¾ {@link AiPlatformEnum}
     */
    private String platform;
    /**
     * API åœ°å€
     */
    private String url;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link CommonStatusEnum}
     * çŠ¶æ€ï¼Œæžšä¸¾ {@link CommonStatusEnum}
     */
    private Integer status;
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java
@@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -12,77 +12,49 @@
/**
 * AI æ¨¡åž‹ DO
 *
 * é»˜è®¤æ¨¡åž‹ï¼š{@link #status} ä¸ºå¼€å¯ï¼Œå¹¶ä¸” {@link #sort} æŽ’序第一
 *
 * @author fansili
 * @since 2024/4/24 19:39
 */
@TableName("ai_model")
@KeySequence("ai_model_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@KeySequence("ai_model_seq")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AiModelDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * API ç§˜é’¥ç¼–号
     *
     * å…³è” {@link AiApiKeyDO#getId()}
     * API ç§˜é’¥ç¼–号,关联 {@link AiApiKeyDO#getId()}
     */
    private Long keyId;
    /**
     * æ¨¡åž‹åç§°
     */
    private String name;
    /**
     * æ¨¡åž‹æ ‡å¿—
     */
    private String model;
    /**
     * å¹³å°
     *
     * æžšä¸¾ {@link AiPlatformEnum}
     * å¹³å°ï¼Œæžšä¸¾ {@link AiPlatformEnum}
     */
    private String platform;
    /**
     * ç±»åž‹
     *
     * æžšä¸¾ {@link AiModelTypeEnum}
     * ç±»åž‹ï¼Œæžšä¸¾ {@link AiModelTypeEnum}
     */
    private Integer type;
    /**
     * æŽ’序值
     */
    private Integer sort;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link CommonStatusEnum}
     * çŠ¶æ€ï¼Œæžšä¸¾ {@link CommonStatusEnum}
     */
    private Integer status;
    // ========== å¯¹è¯é…ç½® ==========
    /**
     * æ¸©åº¦å‚æ•°
     *
     * ç”¨äºŽè°ƒæ•´ç”Ÿæˆå›žå¤çš„随机性和多样性程度:较低的温度值会使输出更收敛于高频词汇,较高的则增加多样性
     */
    private Double temperature;
    /**
     * å•条回复的最大 Token æ•°é‡
     */
    private Integer maxTokens;
    /**
     * ä¸Šä¸‹æ–‡çš„æœ€å¤§ Message æ•°é‡
     */
    private Integer maxContexts;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/workflow/AiWorkflowDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java
@@ -7,15 +7,9 @@
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * AI çŸ¥è¯†åº“文档 Mapper
 *
 * @author xiaoxin
 */
@Mapper
public interface AiKnowledgeDocumentMapper extends BaseMapperX<AiKnowledgeDocumentDO> {
@@ -30,10 +24,6 @@
        update(new LambdaUpdateWrapper<AiKnowledgeDocumentDO>()
                .setSql(" retrieval_count = retrieval_count + 1")
                .in(AiKnowledgeDocumentDO::getId, ids));
    }
    default List<AiKnowledgeDocumentDO> selectListByStatus(Integer status) {
        return selectList(AiKnowledgeDocumentDO::getStatus, status);
    }
    default List<AiKnowledgeDocumentDO> selectListByKnowledgeId(Long knowledgeId) {
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java
@@ -6,14 +6,8 @@
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * AI çŸ¥è¯†åº“ Mapper
 *
 * @author xiaoxin
 */
@Mapper
public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java
@@ -10,21 +10,15 @@
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * AI çŸ¥è¯†åº“分片 Mapper
 *
 * @author xiaoxin
 */
@Mapper
public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegmentDO> {
    default PageResult<AiKnowledgeSegmentDO> selectPage(AiKnowledgeSegmentPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
                .eq(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId())
                .eqIfPresent(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId())
                .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getContent())
                .eqIfPresent(AiKnowledgeSegmentDO::getStatus, reqVO.getStatus())
                .orderByDesc(AiKnowledgeSegmentDO::getId));
@@ -38,8 +32,7 @@
    default List<AiKnowledgeSegmentDO> selectListByDocumentId(Long documentId) {
        return selectList(new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
                .eq(AiKnowledgeSegmentDO::getDocumentId, documentId)
                .orderByDesc(AiKnowledgeSegmentDO::getId));
                .eq(AiKnowledgeSegmentDO::getDocumentId, documentId));
    }
    default List<AiKnowledgeSegmentDO> selectListByKnowledgeIdAndStatus(Long knowledgeId, Integer status) {
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java
@@ -3,33 +3,26 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * AI API å¯†é’¥ Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface AiApiKeyMapper extends BaseMapperX<AiApiKeyDO> {
    default PageResult<AiApiKeyDO> selectPage(AiApiKeyPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<AiApiKeyDO>()
                .likeIfPresent(AiApiKeyDO::getName, reqVO.getName())
                .eqIfPresent(AiApiKeyDO::getPlatform, reqVO.getPlatform())
                .eqIfPresent(AiApiKeyDO::getStatus, reqVO.getStatus())
                .orderByDesc(AiApiKeyDO::getId));
    default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) {
        return selectOne(new LambdaQueryWrapperX<AiApiKeyDO>()
                .eq(AiApiKeyDO::getPlatform, platform)
                .eq(AiApiKeyDO::getStatus, status)
                .last("LIMIT 1"));
    }
    default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) {
        return selectOne(new QueryWrapperX<AiApiKeyDO>()
                .eq("platform", platform)
                .eq("status", status)
                .limitN(1)
                .orderByAsc("id"));
    default PageResult<AiApiKeyDO> selectPage(AiApiKeyPageReqVO pageReqVO) {
        return selectPage(pageReqVO, new LambdaQueryWrapperX<AiApiKeyDO>()
                .likeIfPresent(AiApiKeyDO::getName, pageReqVO.getName())
                .eqIfPresent(AiApiKeyDO::getPlatform, pageReqVO.getPlatform())
                .eqIfPresent(AiApiKeyDO::getStatus, pageReqVO.getStatus())
                .orderByDesc(AiApiKeyDO::getId));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatRoleMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiModelMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.ai.dal.mysql.model;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface AiModelMapper extends BaseMapperX<AiModelDO> {
    default AiModelDO selectFirstByStatus(Integer type, Integer status) {
        return selectOne(new LambdaQueryWrapperX<AiModelDO>()
                .eq(AiModelDO::getType, type)
                .eq(AiModelDO::getStatus, status)
                .orderByAsc(AiModelDO::getSort)
                .last("LIMIT 1"));
    }
    default PageResult<AiModelDO> selectPage(AiModelPageReqVO pageReqVO) {
        return selectPage(pageReqVO, new LambdaQueryWrapperX<AiModelDO>()
                .eqIfPresent(AiModelDO::getType, pageReqVO.getType())
                .eqIfPresent(AiModelDO::getStatus, pageReqVO.getStatus())
                .eqIfPresent(AiModelDO::getPlatform, pageReqVO.getPlatform())
                .likeIfPresent(AiModelDO::getName, pageReqVO.getName())
                .orderByAsc(AiModelDO::getSort));
    }
    default List<AiModelDO> selectListByStatusAndType(Integer status, Integer type, String platform) {
        return selectList(new LambdaQueryWrapperX<AiModelDO>()
                .eqIfPresent(AiModelDO::getStatus, status)
                .eqIfPresent(AiModelDO::getType, type)
                .eqIfPresent(AiModelDO::getPlatform, platform));
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/workflow/AiWorkflowMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/AiDocumentSplitStrategyEnum.java
@@ -5,49 +5,18 @@
/**
 * AI çŸ¥è¯†åº“文档切片策略枚举
 *
 * @author runzhen
 */
@AllArgsConstructor
@Getter
public enum AiDocumentSplitStrategyEnum {
    /**
     * è‡ªåŠ¨è¯†åˆ«æ–‡æ¡£ç±»åž‹å¹¶é€‰æ‹©æœ€ä½³åˆ‡ç‰‡ç­–ç•¥
     */
    AUTO("auto", "自动识别"),
    /**
     * åŸºäºŽ Token æ•°é‡æœºæ¢°åˆ‡åˆ†ï¼ˆé»˜è®¤ç­–略)
     */
    TOKEN("token", "Token åˆ‡åˆ†"),
    /**
     * æŒ‰æ®µè½åˆ‡åˆ†ï¼ˆä»¥åŒæ¢è¡Œç¬¦ä¸ºåˆ†éš”)
     */
    PARAGRAPH("paragraph", "段落切分"),
    /**
     * Markdown QA æ ¼å¼ä¸“用切片器
     * è¯†åˆ«äºŒçº§æ ‡é¢˜ä½œä¸ºé—®é¢˜ï¼Œä¿æŒé—®ç­”对完整性
     * é•¿ç­”案智能切分但保留问题作为上下文
     */
    MARKDOWN_QA("markdown_qa", "Markdown QA åˆ‡åˆ†"),
    /**
     * è¯­ä¹‰åŒ–切分,保留句子完整性
     * åœ¨æ®µè½å’Œå¥å­è¾¹ç•Œå¤„切分,避免截断
     */
    SEMANTIC("semantic", "语义切分");
    /**
     * ç­–略代码
     */
    private final String code;
    /**
     * ç­–略名称
     */
    private final String name;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
@@ -4,66 +4,25 @@
/**
 * AI é”™è¯¯ç æžšä¸¾ç±»
 * <p>
 * ai ç³»ç»Ÿï¼Œä½¿ç”¨ 1-040-000-000 æ®µ
 */
public interface ErrorCodeConstants {
    // ========== API å¯†é’¥ 1-040-000-000 ==========
    // ========== API å¯†é’¥ ==========
    ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API å¯†é’¥ä¸å­˜åœ¨");
    ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API å¯†é’¥å·²ç¦ç”¨ï¼");
    ErrorCode API_CONFIG_PLACEHOLDER_NOT_RESOLVED = new ErrorCode(1_040_000_002, "AI é…ç½®({})无法解析,请检查环境变量或配置项");
    // ========== API æ¨¡åž‹ 1-040-001-000 ==========
    // ========== API æ¨¡åž‹ ==========
    ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!");
    ErrorCode MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!");
    ErrorCode MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认模型");
    ErrorCode MODEL_USE_TYPE_ERROR = new ErrorCode(1_040_001_003, "操作失败,该模型的模型类型不正确");
    // ========== API èŠå¤©è§’色 1-040-002-000 ==========
    ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在");
    ErrorCode CHAT_ROLE_DISABLE = new ErrorCode(1_040_001_001, "聊天角色({})已禁用!");
    // ========== API èŠå¤©ä¼šè¯ 1-040-003-000 ==========
    ErrorCode CHAT_CONVERSATION_NOT_EXISTS = new ErrorCode(1_040_003_000, "对话不存在!");
    ErrorCode CHAT_CONVERSATION_MODEL_ERROR = new ErrorCode(1_040_003_001, "操作失败,该聊天模型的配置不完整");
    // ========== API èŠå¤©æ¶ˆæ¯ 1-040-004-000 ==========
    ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
    ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!");
    // ========== API ç»˜ç”» 1-040-005-000 ==========
    ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_040_005_000, "图片不存在!");
    ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_040_005_001, "Midjourney æäº¤å¤±è´¥!原因:{}");
    ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_040_005_002, "Midjourney æŒ‰é’® customId ä¸å­˜åœ¨! {}");
    // ========== API éŸ³ä¹ 1-040-006-000 ==========
    ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_040_006_000, "音乐不存在!");
    // ========== API å†™ä½œ 1-040-007-000 ==========
    ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_040_007_000, "作文不存在!");
    ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_040_07_001, "写作生成异常!");
    // ========== API æ€ç»´å¯¼å›¾ 1-040-008-000 ==========
    ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!");
    // ========== API çŸ¥è¯†åº“ 1-040-009-000 ==========
    // ========== API çŸ¥è¯†åº“ ==========
    ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_040_009_000, "知识库不存在!");
    ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_040_009_101, "文档不存在!");
    ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_040_009_102, "文档内容为空!");
    ErrorCode KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL = new ErrorCode(1_040_009_102, "文件下载失败!");
    ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_040_009_102, "文档加载失败!");
    ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_040_009_202, "段落不存在!");
    ErrorCode KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG = new ErrorCode(1_040_009_203, "内容 Token æ•°ä¸º {},超过最大限制 {}");
    // ========== AI å·¥å…· 1-040-010-000 ==========
    ErrorCode TOOL_NOT_EXISTS = new ErrorCode(1_040_010_000, "工具不存在");
    ErrorCode TOOL_NAME_NOT_EXISTS = new ErrorCode(1_040_010_001, "工具({})找不到 Bean");
    // ========== AI å·¥ä½œæµ 1-040-011-000 ==========
    ErrorCode WORKFLOW_NOT_EXISTS = new ErrorCode(1_040_011_000, "工作流不存在");
    ErrorCode WORKFLOW_CODE_EXISTS = new ErrorCode(1_040_011_001, "工作流标识已存在");
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelTypeEnum.java
@@ -3,13 +3,10 @@
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * AI æ¨¡åž‹ç±»åž‹çš„æžšä¸¾
 *
 * @author èŠ‹é“æºç 
 */
@Getter
@RequiredArgsConstructor
@@ -22,20 +19,12 @@
    EMBEDDING(5, "向量"),
    RERANK(6, "重排序");
    /**
     * ç±»åž‹
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(AiModelTypeEnum::getType).toArray(Integer[]::new);
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public Integer[] array() { return ARRAYS; }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java
@@ -3,71 +3,33 @@
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * AI æ¨¡åž‹å¹³å°
 *
 * @author fansili
 */
@Getter
@AllArgsConstructor
public enum AiPlatformEnum implements ArrayValuable<String> {
    // ========== å›½å†…平台 ==========
    TONG_YI("TongYi", "通义千问"),
    DEEP_SEEK("DeepSeek", "DeepSeek"),
    OPENAI("OpenAI", "OpenAI"),
    OLLAMA("Ollama", "Ollama");
    TONG_YI("TongYi", "通义千问"), // é˜¿é‡Œ
    YI_YAN("YiYan", "文心一言"), // ç™¾åº¦
    DEEP_SEEK("DeepSeek", "DeepSeek"), // DeepSeek
    ZHI_PU("ZhiPu", "智谱"), // æ™ºè°± AI
    XING_HUO("XingHuo", "星火"), // è®¯é£ž
    DOU_BAO("DouBao", "豆包"), // å­—节
    HUN_YUAN("HunYuan", "混元"), // è…¾è®¯
    SILICON_FLOW("SiliconFlow", "硅基流动"), // ç¡…基流动
    MINI_MAX("MiniMax", "MiniMax"), // ç¨€å®‡ç§‘技
    MOONSHOT("Moonshot", "月之暗面"), // KIMI
    BAI_CHUAN("BaiChuan", "百川智能"), // ç™¾å·æ™ºèƒ½
    STEP_FUN("StepFun", "阶跃星辰"), // é˜¶è·ƒæ˜Ÿè¾°
    // ========== å›½å¤–平台 ==========
    OPENAI("OpenAI", "OpenAI"), // OpenAI å®˜æ–¹
    AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI å¾®è½¯
    ANTHROPIC("Anthropic", "Anthropic"), // Anthropic Claude
    GEMINI("Gemini", "Gemini"), // è°·æ­Œ Gemini
    OLLAMA("Ollama", "Ollama"),
    STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
    MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney
    SUNO("Suno", "Suno"), // Suno AI
    GROK("Grok", "Grok"), // Grok
    ;
    /**
     * å¹³å°
     */
    private final String platform;
    /**
     * å¹³å°å
     */
    private final String name;
    public static final String[] ARRAYS = Arrays.stream(values()).map(AiPlatformEnum::getPlatform).toArray(String[]::new);
    public static AiPlatformEnum validatePlatform(String platform) {
        for (AiPlatformEnum platformEnum : AiPlatformEnum.values()) {
            if (platformEnum.getPlatform().equals(platform)) {
                return platformEnum;
            }
        for (AiPlatformEnum e : values()) {
            if (e.getPlatform().equals(platform)) return e;
        }
        throw new IllegalArgumentException("非法平台: " + platform);
    }
    @Override
    public String[] array() {
        return ARRAYS;
    }
    public String[] array() { return ARRAYS; }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
@@ -1,77 +1,47 @@
package cn.iocoder.yudao.module.ai.framework.ai.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.minimax.MiniMaxChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.moonshot.MoonshotChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.stepfun.StepFunChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.yiyan.YiYanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.zhipu.ZhiPuChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
import cn.iocoder.yudao.module.ai.tool.method.PersonService;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.embedding.text.DashScopeEmbeddingOptions;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import io.micrometer.observation.ObservationRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
 * èЋ道 AI è‡ªåŠ¨é…ç½®
 *
 * @author fansili
 * ä½¿ç”¨ OpenAI å…¼å®¹æŽ¥å£å¯¹æŽ¥é€šä¹‰åƒé—® DashScope
 */
@Configuration
@EnableConfigurationProperties({ YudaoAiProperties.class,
        QdrantVectorStoreProperties.class, // è§£æž Qdrant é…ç½®
        RedisVectorStoreProperties.class, // è§£æž Redis é…ç½®
        MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // è§£æž Milvus é…ç½®
@EnableConfigurationProperties({
        YudaoAiProperties.class,
        QdrantVectorStoreProperties.class,
        RedisVectorStoreProperties.class,
        MilvusVectorStoreProperties.class,
        MilvusServiceClientProperties.class
})
@Slf4j
public class AiAutoConfiguration {
    private static final String DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
    @Bean
    public AiModelFactory aiModelFactory() {
@@ -81,400 +51,46 @@
    @Bean
    @ConditionalOnMissingBean
    public ObservationRegistry observationRegistry() {
        // ç‰¹æ®Šï¼šå…œåº•有 ObservationRegistry Bean,避免相关的 ChatModel åˆ›å»ºæŠ¥é”™ã€‚相关 issue:https://t.zsxq.com/CuPu4
        return ObservationRegistry.NOOP;
    }
    // ========== å„种 AI Client åˆ›å»º ==========
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "spring.ai.dashscope.api-key")
    public DashScopeChatModel dashScopeChatModel(@Value("${spring.ai.dashscope.api-key}") String apiKey,
                                                 ToolCallingManager toolCallingManager,
                                                 ObservationRegistry observationRegistry) {
        return buildTongYiChatModel(apiKey, toolCallingManager, observationRegistry);
    }
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "spring.ai.dashscope.api-key")
    public DashScopeImageModel dashScopeImageModel(@Value("${spring.ai.dashscope.api-key}") String apiKey,
                                                   ObservationRegistry observationRegistry) {
        return buildTongYiImagesModel(apiKey, observationRegistry);
    }
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "spring.ai.dashscope.api-key")
    public DashScopeEmbeddingModel dashScopeEmbeddingModel(@Value("${spring.ai.dashscope.api-key}") String apiKey,
                                                           ObservationRegistry observationRegistry) {
        return buildTongYiEmbeddingModel(apiKey, null, observationRegistry);
    }
    public static DashScopeChatModel buildTongYiChatModel(String apiKey) {
        return buildTongYiChatModel(apiKey, getToolCallingManager(), getObservationRegistry());
    }
    private static DashScopeChatModel buildTongYiChatModel(String apiKey, ToolCallingManager toolCallingManager,
                                                           ObservationRegistry observationRegistry) {
        DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
        DashScopeChatOptions options = DashScopeChatOptions.builder()
                .model(DashScopeApi.DEFAULT_CHAT_MODEL)
                .temperature(0.7)
                .build();
        return new DashScopeChatModel(dashScopeApi, options, toolCallingManager, RetryUtils.DEFAULT_RETRY_TEMPLATE,
                observationRegistry);
    }
    public static DashScopeImageModel buildTongYiImagesModel(String apiKey) {
        return buildTongYiImagesModel(apiKey, getObservationRegistry());
    }
    private static DashScopeImageModel buildTongYiImagesModel(String apiKey, ObservationRegistry observationRegistry) {
        DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(apiKey).build();
        DashScopeImageOptions options = DashScopeImageOptions.builder()
                .model(DashScopeImageApi.DEFAULT_IMAGE_MODEL)
                .build();
        return new DashScopeImageModel(dashScopeImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE,
                observationRegistry);
    }
    public static DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
        return buildTongYiEmbeddingModel(apiKey, model, getObservationRegistry());
    }
    private static DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model,
                                                                     ObservationRegistry observationRegistry) {
        DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build();
        DashScopeEmbeddingOptions options = DashScopeEmbeddingOptions.builder()
                .model(StrUtil.blankToDefault(model, DashScopeApi.DEFAULT_EMBEDDING_MODEL))
                .build();
        return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, options, RetryUtils.DEFAULT_RETRY_TEMPLATE,
                observationRegistry);
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
    public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao();
        return buildDouBaoChatClient(properties);
    }
    public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
        }
        DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
                .deepSeekApi(DeepSeekApi.builder()
                        .baseUrl(DouBaoChatModel.BASE_URL)
                        .completionsPath(DouBaoChatModel.COMPLETE_PATH)
                        .apiKey(properties.getApiKey())
                        .build())
                .options(DeepSeekChatOptions.builder()
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
        return new DouBaoChatModel(openAiChatModel);
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
    public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow();
        return buildSiliconFlowChatClient(properties);
    }
    public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
        }
        DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
                .deepSeekApi(DeepSeekApi.builder()
                        .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
                        .apiKey(properties.getApiKey())
                        .build())
                .options(DeepSeekChatOptions.builder()
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
        return new SiliconFlowChatModel(openAiChatModel);
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
    public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan();
        return buildHunYuanChatClient(properties);
    }
    public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
        }
        if (StrUtil.isEmpty(properties.getBaseUrl())) {
            properties.setBaseUrl(HunYuanChatModel.BASE_URL);
        }
        // åˆ›å»º DeepSeekChatModel、HunYuanChatModel å¯¹è±¡
        DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
                .deepSeekApi(DeepSeekApi.builder()
                        .baseUrl(properties.getBaseUrl())
                        .completionsPath(HunYuanChatModel.COMPLETE_PATH)
                        .apiKey(properties.getApiKey())
                        .build())
                .options(DeepSeekChatOptions.builder()
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
        return new HunYuanChatModel(openAiChatModel);
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
    public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo();
        return buildXingHuoChatClient(properties);
    }
    public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
        }
        return XingHuoChatModel.builder()
                .apiKey(properties.getApiKey())
                .options(DeepSeekChatOptions.builder()
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
    public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan();
        return buildBaiChuanChatClient(properties);
    }
    public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
        }
        DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
                .deepSeekApi(DeepSeekApi.builder()
                        .baseUrl(BaiChuanChatModel.BASE_URL)
                        .apiKey(properties.getApiKey())
                        .completionsPath(BaiChuanChatModel.COMPLETE_PATH)
                        .build())
                .options(DeepSeekChatOptions.builder()
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
        return new BaiChuanChatModel(deepSeekChatModel);
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true")
    public YiYanChatModel yiYanChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.YiYan properties = yudaoAiProperties.getYiyan();
        return buildYiYanChatClient(properties);
    }
    public YiYanChatModel buildYiYanChatClient(YudaoAiProperties.YiYan properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(YiYanChatModel.MODEL_DEFAULT);
        }
        return new YiYanChatModel(buildDeepSeekCompatibleChatModel(
                StrUtil.blankToDefault(properties.getBaseUrl(), YiYanChatModel.BASE_URL),
                null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
                properties.getMaxTokens(), properties.getTopP()));
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.zhipu.enable", havingValue = "true")
    public ZhiPuChatModel zhiPuChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.ZhiPu properties = yudaoAiProperties.getZhipu();
        return buildZhiPuChatClient(properties);
    }
    public ZhiPuChatModel buildZhiPuChatClient(YudaoAiProperties.ZhiPu properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(ZhiPuChatModel.MODEL_DEFAULT);
        }
        DeepSeekChatModel deepSeekChatModel = DeepSeekChatModel.builder()
                .deepSeekApi(DeepSeekApi.builder()
                        .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), ZhiPuChatModel.BASE_URL))
                        .apiKey(properties.getApiKey())
                        .build())
                .options(DeepSeekChatOptions.builder()
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
        return new ZhiPuChatModel(deepSeekChatModel);
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.minimax.enable", havingValue = "true")
    public MiniMaxChatModel miniMaxChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.MiniMax properties = yudaoAiProperties.getMinimax();
        return buildMiniMaxChatClient(properties);
    }
    public MiniMaxChatModel buildMiniMaxChatClient(YudaoAiProperties.MiniMax properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(MiniMaxChatModel.MODEL_DEFAULT);
        }
        return new MiniMaxChatModel(buildDeepSeekCompatibleChatModel(
                StrUtil.blankToDefault(properties.getBaseUrl(), MiniMaxChatModel.BASE_URL),
                null, properties.getApiKey(), properties.getModel(), properties.getTemperature(),
                properties.getMaxTokens(), properties.getTopP()));
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.moonshot.enable", havingValue = "true")
    public MoonshotChatModel moonshotChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.Moonshot properties = yudaoAiProperties.getMoonshot();
        return buildMoonshotChatClient(properties);
    }
    public MoonshotChatModel buildMoonshotChatClient(YudaoAiProperties.Moonshot properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(MoonshotChatModel.MODEL_DEFAULT);
        }
        return new MoonshotChatModel(buildDeepSeekCompatibleChatModel(
                StrUtil.blankToDefault(properties.getBaseUrl(), MoonshotChatModel.BASE_URL),
                MoonshotChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
                properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.stepfun.enable", havingValue = "true")
    public StepFunChatModel stepFunChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.StepFun properties = yudaoAiProperties.getStepfun();
        return buildStepFunChatClient(properties);
    }
    public StepFunChatModel buildStepFunChatClient(YudaoAiProperties.StepFun properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(StepFunChatModel.MODEL_DEFAULT);
        }
        return new StepFunChatModel(buildDeepSeekCompatibleChatModel(
                StrUtil.blankToDefault(properties.getBaseUrl(), StepFunChatModel.BASE_URL),
                StepFunChatModel.COMPLETE_PATH, properties.getApiKey(), properties.getModel(),
                properties.getTemperature(), properties.getMaxTokens(), properties.getTopP()));
    }
    private static DeepSeekChatModel buildDeepSeekCompatibleChatModel(String baseUrl, String completionsPath,
                                                                     String apiKey, String model,
                                                                     Double temperature, Integer maxTokens,
                                                                     Double topP) {
        DeepSeekApi.Builder apiBuilder = DeepSeekApi.builder()
                .baseUrl(baseUrl)
                .apiKey(apiKey);
        if (StrUtil.isNotEmpty(completionsPath)) {
            apiBuilder.completionsPath(completionsPath);
        }
        return DeepSeekChatModel.builder()
                .deepSeekApi(apiBuilder.build())
                .options(DeepSeekChatOptions.builder()
                        .model(model)
                        .temperature(temperature)
                        .maxTokens(maxTokens)
                        .topP(topP)
                        .build())
                .build();
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
    public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney();
        return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true")
    public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
        return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
    }
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.grok.enable", havingValue = "true")
    public GrokChatModel grokChatClient(YudaoAiProperties yudaoAiProperties) {
        YudaoAiProperties.Grok properties = yudaoAiProperties.getGrok();
        return buildGrokChatClient(properties);
    }
    public GrokChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) {
        if (StrUtil.isEmpty(properties.getModel())) {
            properties.setModel(GrokChatModel.MODEL_DEFAULT);
        }
        OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
                .options(OpenAiChatOptions.builder()
                        .baseUrl(StrUtil.blankToDefault(properties.getBaseUrl(), GrokChatModel.BASE_URL))
                        .apiKey(properties.getApiKey())
                        .model(properties.getModel())
                        .temperature(properties.getTemperature())
                        .maxTokens(properties.getMaxTokens())
                        .topP(properties.getTopP())
                        .build())
                .build();
        return new GrokChatModel(openAiChatModel);
    }
    // ========== RAG ç›¸å…³ ==========
    @Bean
    public TokenCountEstimator tokenCountEstimator() {
        return new JTokkitTokenCountEstimator();
    }
    @Bean
    public BatchingStrategy batchingStrategy() {
        return new TokenCountBatchingStrategy();
    }
    private static ToolCallingManager getToolCallingManager() {
        return SpringUtil.getBean(ToolCallingManager.class);
    }
    private static ObservationRegistry getObservationRegistry() {
        return SpringUtil.getBean(ObservationRegistry.class);
    }
    // ========== Web Search ç›¸å…³ ==========
    @Bean
    @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true")
    public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) {
        return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
    @ConditionalOnMissingBean
    public TokenCountEstimator tokenCountEstimator() {
        return new JTokkitTokenCountEstimator();
    }
    // ========== MCP ç›¸å…³ ==========
    // ========== é€šä¹‰åƒé—® Chat(通过 OpenAI å…¼å®¹æŽ¥å£ï¼‰==========
    /**
     * å‚考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</>
     */
    @Bean
    public List<ToolCallback> toolCallbacks(PersonService personService) {
        return List.of(ToolCallbacks.from(personService));
    public static OpenAiChatModel buildTongYiChatModel(String apiKey, String model) {
        return OpenAiChatModel.builder()
                .options(OpenAiChatOptions.builder()
                        .baseUrl(DASHSCOPE_BASE_URL)
                        .apiKey(apiKey)
                        .model(StrUtil.blankToDefault(model, "qwen-plus"))
                        .temperature(0.7)
                        .build())
                .toolCallingManager(ToolCallingManager.builder().build())
                .build();
    }
    // ========== é€šä¹‰åƒé—® Embedding(通过 OpenAI å…¼å®¹æŽ¥å£ï¼‰==========
    public static OpenAiEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
        return OpenAiEmbeddingModel.builder()
                .options(OpenAiEmbeddingOptions.builder()
                        .baseUrl(DASHSCOPE_BASE_URL)
                        .apiKey(apiKey)
                        .model(StrUtil.blankToDefault(model, "text-embedding-v3"))
                        .build())
                .metadataMode(MetadataMode.EMBED)
                .build();
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
@@ -5,262 +5,17 @@
/**
 * èЋ道 AI é…ç½®ç±»
 *
 * @author fansili
 * @since 1.0
 */
@ConfigurationProperties(prefix = "yudao.ai")
@Data
public class YudaoAiProperties {
    /**
     * å­—节豆包
     */
    private DouBao doubao;
    /**
     * è…¾è®¯æ··å…ƒ
     */
    private HunYuan hunyuan;
    /**
     * ç¡…基流动
     */
    private SiliconFlow siliconflow;
    /**
     * è®¯é£žæ˜Ÿç«
     */
    private XingHuo xinghuo;
    /**
     * ç™¾å·
     */
    private BaiChuan baichuan;
    /**
     * æ–‡å¿ƒä¸€è¨€
     */
    private YiYan yiyan;
    /**
     * æ™ºè°±
     */
    private ZhiPu zhipu;
    /**
     * MiniMax
     */
    private MiniMax minimax;
    /**
     * æœˆä¹‹æš—面
     */
    private Moonshot moonshot;
    /**
     * é˜¶è·ƒæ˜Ÿè¾°
     */
    private StepFun stepfun;
    /**
     * Grok
     */
    private Grok grok;
    /**
     * Midjourney ç»˜å›¾
     */
    private Midjourney midjourney;
    /**
     * Suno éŸ³ä¹
     */
    @SuppressWarnings("SpellCheckingInspection")
    private Suno suno;
    /**
     * ç½‘络搜索
     */
    private WebSearch webSearch;
    @Data
    public static class DouBao {
        private String enable;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class HunYuan {
        private String enable;
        private String baseUrl;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class SiliconFlow {
        private String enable;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class XingHuo {
        private String enable;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class BaiChuan {
        private String enable;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class YiYan {
        private String enable;
        private String baseUrl;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class ZhiPu {
        private String enable;
        private String baseUrl;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class MiniMax {
        private String enable;
        private String baseUrl;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class Moonshot {
        private String enable;
        private String baseUrl;
        private String apiKey;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class Midjourney {
        private String enable;
        private String baseUrl;
        private String apiKey;
        private String notifyUrl;
    }
    @Data
    public static class Suno {
        private boolean enable;
        private String baseUrl;
    }
    @Data
    public static class Grok {
        private String enable;
        private String apiKey;
        private String baseUrl;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class StepFun {
        private String enable;
        private String apiKey;
        private String baseUrl;
        private String model;
        private Double temperature;
        private Integer maxTokens;
        private Double topP;
    }
    @Data
    public static class WebSearch {
        private boolean enable;
        private String apiKey;
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java
@@ -1,111 +1,22 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.vectorstore.VectorStore;
import java.util.Map;
/**
 * AI Model æ¨¡åž‹å·¥åŽ‚çš„æŽ¥å£ç±»
 *
 * @author fansili
 */
public interface AiModelFactory {
    /**
     * åŸºäºŽæŒ‡å®šé…ç½®ï¼ŒèŽ·å¾— ChatModel å¯¹è±¡
     *
     * å¦‚果不存在,则进行创建
     *
     * @param platform å¹³å°
     * @param apiKey API KEY
     * @param url API URL
     * @return ChatModel å¯¹è±¡
     */
    ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url);
    ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url, String model);
    /**
     * åŸºäºŽé»˜è®¤é…ç½®ï¼ŒèŽ·å¾— ChatModel å¯¹è±¡
     *
     * é»˜è®¤é…ç½®ï¼ŒæŒ‡çš„æ˜¯åœ¨ application.yaml é…ç½®æ–‡ä»¶ä¸­çš„ spring.ai ç›¸å…³çš„配置
     *
     * @param platform å¹³å°
     * @return ChatModel å¯¹è±¡
     */
    ChatModel getDefaultChatModel(AiPlatformEnum platform);
    /**
     * åŸºäºŽé»˜è®¤é…ç½®ï¼ŒèŽ·å¾— ImageModel å¯¹è±¡
     *
     * é»˜è®¤é…ç½®ï¼ŒæŒ‡çš„æ˜¯åœ¨ application.yaml é…ç½®æ–‡ä»¶ä¸­çš„ spring.ai ç›¸å…³çš„配置
     *
     * @param platform å¹³å°
     * @return ImageModel å¯¹è±¡
     */
    ImageModel getDefaultImageModel(AiPlatformEnum platform);
    /**
     * åŸºäºŽæŒ‡å®šé…ç½®ï¼ŒèŽ·å¾— ImageModel å¯¹è±¡
     *
     * å¦‚果不存在,则进行创建
     *
     * @param platform å¹³å°
     * @param apiKey API KEY
     * @param url API URL
     * @return ImageModel å¯¹è±¡
     */
    ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url);
    /**
     * åŸºäºŽæŒ‡å®šé…ç½®ï¼ŒèŽ·å¾— MidjourneyApi å¯¹è±¡
     *
     * å¦‚果不存在,则进行创建
     *
     * @param apiKey API KEY
     * @param url API URL
     * @return MidjourneyApi å¯¹è±¡
     */
    MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url);
    /**
     * åŸºäºŽæŒ‡å®šé…ç½®ï¼ŒèŽ·å¾— SunoApi å¯¹è±¡
     *
     * å¦‚果不存在,则进行创建
     *
     * @param apiKey API KEY
     * @param url API URL
     * @return SunoApi å¯¹è±¡
     */
    SunoApi getOrCreateSunoApi(String apiKey, String url);
    /**
     * åŸºäºŽæŒ‡å®šé…ç½®ï¼ŒèŽ·å¾— EmbeddingModel å¯¹è±¡
     *
     * å¦‚果不存在,则进行创建
     *
     * @param platform å¹³å°
     * @param apiKey   API KEY
     * @param url      API URL
     * @param model     æ¨¡åž‹
     * @return ChatModel å¯¹è±¡
     */
    EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model);
    /**
     * åŸºäºŽæŒ‡å®šé…ç½®ï¼ŒèŽ·å¾— VectorStore å¯¹è±¡
     *
     * å¦‚果不存在,则进行创建
     *
     * @param type           å‘量存储类型
     * @param embeddingModel å‘量模型
     * @param metadataFields å…ƒæ•°æ®å­—段
     * @return VectorStore å¯¹è±¡
     */
    VectorStore getOrCreateVectorStore(Class<? extends VectorStore> type,
                                       EmbeddingModel embeddingModel,
                                       Map<String, Class<?>> metadataFields);
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/baichuan/BaiChuanChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/grok/GrokChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/midjourney/api/MidjourneyApi.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/minimax/MiniMaxChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/moonshot/MoonshotChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowApiConstants.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageApi.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageOptions.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/stepfun/StepFunChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/suno/api/SunoApi.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/wenduoduo/api/WenDuoDuoPptApi.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/api/XunFeiPptApi.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/yiyan/YiYanChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/zhipu/ZhiPuChatModel.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/web/config/AiWebConfiguration.java
@@ -6,16 +6,11 @@
import org.springframework.context.annotation.Configuration;
/**
 * ai æ¨¡å—çš„ web ç»„ä»¶çš„ Configuration
 *
 * @author èŠ‹é“æºç 
 * AI æ¨¡å—çš„ web ç»„ä»¶ Configuration
 */
@Configuration(proxyBeanMethods = false)
public class AiWebConfiguration {
    /**
     * ai æ¨¡å—çš„ API åˆ†ç»„
     */
    @Bean
    public GroupedOpenApi aiGroupedOpenApi() {
        return YudaoSwaggerAutoConfiguration.buildGroupedOpenApi("ai");
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/job/image/AiMidjourneySyncJob.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java
@@ -1,126 +1,41 @@
package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.*;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * AI çŸ¥è¯†åº“文档 Service æŽ¥å£
 *
 * @author xiaoxin
 */
public interface AiKnowledgeDocumentService {
    /**
     * åˆ›å»ºæ–‡æ¡£ï¼ˆå•个)
     *
     * @param createReqVO æ–‡æ¡£åˆ›å»º Request VO
     * @return æ–‡æ¡£ç¼–号
     */
    Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO);
    List<Long> createDocuments(@Valid AiKnowledgeDocumentCreateReqVO createReqVO);
    void updateDocument(@Valid AiKnowledgeDocumentUpdateReqVO updateReqVO);
    void deleteDocument(Long id);
    AiKnowledgeDocumentDO getDocument(Long id);
    AiKnowledgeDocumentDO validateDocumentExists(Long id);
    PageResult<AiKnowledgeDocumentDO> getDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO);
    void updateDocumentStatus(@Valid AiKnowledgeDocumentUpdateStatusReqVO updateStatusReqVO);
    List<AiKnowledgeSegmentProcessRespVO> getDocumentProcessingProgress(List<Long> documentIds);
    void processDocumentSegments(Long documentId);
    /**
     * åˆ›å»ºæ–‡æ¡£ï¼ˆå¤šä¸ªï¼‰
     * é¢„览文档切分结果(不保存)
     *
     * @param createListReqVO æ‰¹é‡åˆ›å»º Request VO
     * @return æ–‡æ¡£ç¼–号列表
     * @param url              æ–‡æ¡£ URL
     * @param name             æ–‡æ¡£åç§°
     * @param segmentMaxTokens åˆ†æ®µæœ€å¤§ Token æ•°
     * @return åˆ‡åˆ†åŽçš„分段文本列表
     */
    List<Long> createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO);
    /**
     * èŽ·å–æ–‡æ¡£åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µå‚æ•°
     * @return æ–‡æ¡£åˆ†é¡µ
     */
    PageResult<AiKnowledgeDocumentDO> getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO);
    /**
     * èŽ·å–æ–‡æ¡£è¯¦æƒ…
     *
     * @param id æ–‡æ¡£ç¼–号
     * @return æ–‡æ¡£è¯¦æƒ…
     */
    AiKnowledgeDocumentDO getKnowledgeDocument(Long id);
    /**
     * æ›´æ–°æ–‡æ¡£
     *
     * @param reqVO æ›´æ–°ä¿¡æ¯
     */
    void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO);
    /**
     * æ›´æ–°æ–‡æ¡£çŠ¶æ€
     *
     * @param reqVO æ›´æ–°çŠ¶æ€ä¿¡æ¯
     */
    void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO);
    /**
     * åˆ é™¤æ–‡æ¡£
     *
     * @param id æ–‡æ¡£ç¼–号
     */
    void deleteKnowledgeDocument(Long id);
    /**
     * æ ¹æ®çŸ¥è¯†åº“编号,批量删除文档
     *
     * @param knowledgeId çŸ¥è¯†åº“编号
     */
    void deleteKnowledgeDocumentByKnowledgeId(Long knowledgeId);
    /**
     * æ ¡éªŒæ–‡æ¡£æ˜¯å¦å­˜åœ¨
     *
     * @param id æ–‡æ¡£ç¼–号
     * @return æ–‡æ¡£ä¿¡æ¯
     */
    AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id);
    /**
     * è¯»å– URL å†…容
     *
     * @param url URL
     * @return å†…容
     */
    String readUrl(String url);
    /**
     * èŽ·å–æ–‡æ¡£åˆ—è¡¨
     *
     * @param ids æ–‡æ¡£ç¼–号列表
     * @return æ–‡æ¡£åˆ—表
     */
    List<AiKnowledgeDocumentDO> getKnowledgeDocumentList(Collection<Long> ids);
    /**
     * æ ¹æ®çŸ¥è¯†åº“编号获取文档列表
     *
     * @param knowledgeId çŸ¥è¯†åº“编号
     * @return æ–‡æ¡£åˆ—表
     */
    List<AiKnowledgeDocumentDO> getKnowledgeDocumentListByKnowledgeId(Long knowledgeId);
    /**
     * èŽ·å–æ–‡æ¡£ Map
     *
     * @param ids æ–‡æ¡£ç¼–号列表
     * @return æ–‡æ¡£ Map
     */
    default Map<Long, AiKnowledgeDocumentDO> getKnowledgeDocumentMap(Collection<Long> ids) {
        return convertMap(getKnowledgeDocumentList(ids), AiKnowledgeDocumentDO::getId);
    }
    List<String> previewSplit(String url, String name, Integer segmentMaxTokens);
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java
@@ -1,157 +1,33 @@
package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSaveReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.*;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
import org.springframework.scheduling.annotation.Async;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * AI çŸ¥è¯†åº“段落 Service æŽ¥å£
 *
 * @author xiaoxin
 */
public interface AiKnowledgeSegmentService {
    /**
     * èŽ·å–çŸ¥è¯†åº“æ®µè½è¯¦æƒ…
     *
     * @param id æ®µè½ç¼–号
     * @return æ®µè½è¯¦æƒ…
     */
    AiKnowledgeSegmentDO getKnowledgeSegment(Long id);
    AiKnowledgeSegmentDO getSegment(Long id);
    /**
     * èŽ·å–çŸ¥è¯†åº“æ®µè½åˆ—è¡¨
     *
     * @param ids æ®µè½ç¼–号列表
     * @return æ®µè½åˆ—表
     */
    List<AiKnowledgeSegmentDO> getKnowledgeSegmentList(Collection<Long> ids);
    AiKnowledgeSegmentDO validateSegmentExists(Long id);
    /**
     * èŽ·å–çŸ¥è¯†åº“æ®µè½ Map
     *
     * @param ids æ®µè½ç¼–号列表
     * @return æ®µè½ Map
     */
    default Map<Long, AiKnowledgeSegmentDO> getKnowledgeSegmentMap(Collection<Long> ids) {
        return convertMap(getKnowledgeSegmentList(ids), AiKnowledgeSegmentDO::getId);
    }
    PageResult<AiKnowledgeSegmentDO> getSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO);
    /**
     * èŽ·å–æ®µè½åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return æ–‡æ¡£åˆ†é¡µ
     */
    PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO);
    Long createSegment(AiKnowledgeSegmentSaveReqVO saveReqVO);
    /**
     * åŸºäºŽ content å†…容,切片创建多个段落
     *
     * @param documentId çŸ¥è¯†åº“文档编号
     * @param content    æ–‡æ¡£å†…容
     */
    void createKnowledgeSegmentBySplitContent(Long documentId, String content);
    void updateSegment(AiKnowledgeSegmentSaveReqVO saveReqVO);
    /**
     * ã€å¼‚步】基于 content å†…容,切片创建多个段落
     *
     * @param documentId çŸ¥è¯†åº“文档编号
     * @param content    æ–‡æ¡£å†…容
     */
    @Async
    default void createKnowledgeSegmentBySplitContentAsync(Long documentId, String content) {
        createKnowledgeSegmentBySplitContent(documentId, content);
    }
    void deleteSegment(Long id);
    /**
     * åˆ›å»ºçŸ¥è¯†åº“段落
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return æ®µè½ç¼–号
     */
    Long createKnowledgeSegment(AiKnowledgeSegmentSaveReqVO createReqVO);
    void updateSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO updateStatusReqVO);
    /**
     * æ›´æ–°æ®µè½çš„内容
     *
     * @param reqVO æ›´æ–°å†…容
     */
    void updateKnowledgeSegment(AiKnowledgeSegmentSaveReqVO reqVO);
    void deleteSegmentsByDocumentId(Long documentId);
    /**
     * æ›´æ–°æ®µè½çš„状态
     *
     * @param reqVO æ›´æ–°å†…容
     */
    void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
    void saveSegments(List<AiKnowledgeSegmentDO> segments, Long knowledgeId, Long embeddingModelId);
    /**
     * åˆ é™¤çŸ¥è¯†åº“段落
     *
     * @param id æ®µè½ç¼–号
     */
    void deleteKnowledgeSegment(Long id);
    /**
     * é‡æ–°ç´¢å¼•知识库下的所有文档段落
     *
     * @param knowledgeId çŸ¥è¯†åº“编号
     */
    void reindexKnowledgeSegmentByKnowledgeId(Long knowledgeId);
    /**
     * ã€å¼‚步】重新索引知识库下的所有文档段落
     *
     * @param knowledgeId çŸ¥è¯†åº“编号
     */
    @Async
    default void reindexByKnowledgeIdAsync(Long knowledgeId) {
        reindexKnowledgeSegmentByKnowledgeId(knowledgeId);
    }
    /**
     * æ ¹æ®æ–‡æ¡£ç¼–号删除段落
     *
     * @param documentId æ–‡æ¡£ç¼–号
     */
    void deleteKnowledgeSegmentByDocumentId(Long documentId);
    /**
     * æœç´¢çŸ¥è¯†åº“段落,并返回结果
     *
     * @param reqBO æœç´¢è¯·æ±‚信息
     * @return æœç´¢ç»“果段落列表
     */
    List<AiKnowledgeSegmentSearchRespBO> searchKnowledgeSegment(AiKnowledgeSegmentSearchReqBO reqBO);
    /**
     * æ ¹æ® URL å†…容,切片创建多个段落
     *
     * @param url              URL åœ°å€
     * @param segmentMaxTokens æ®µè½æœ€å¤§ Token æ•°
     * @return åˆ‡ç‰‡åŽçš„æ®µè½åˆ—表
     */
    List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens);
    /**
     * èŽ·å–æ–‡æ¡£å¤„ç†è¿›åº¦ï¼ˆå¤šä¸ªï¼‰
     *
     * @param documentIds æ–‡æ¡£ç¼–号列表
     * @return æ–‡æ¡£å¤„理列表
     */
    List<AiKnowledgeSegmentProcessRespVO> getKnowledgeSegmentProcessList(List<Long> documentIds);
    List<AiKnowledgeSegmentSearchRespBO> searchSegments(AiKnowledgeSegmentSearchReqBO searchReqBO);
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java
@@ -4,67 +4,26 @@
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * AI çŸ¥è¯†åº“-基础信息 Service æŽ¥å£
 *
 * @author xiaoxin
 */
public interface AiKnowledgeService {
    /**
     * åˆ›å»ºçŸ¥è¯†åº“
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createKnowledge(AiKnowledgeSaveReqVO createReqVO);
    Long createKnowledge(@Valid AiKnowledgeSaveReqVO createReqVO);
    /**
     * æ›´æ–°çŸ¥è¯†åº“
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO);
    void updateKnowledge(@Valid AiKnowledgeSaveReqVO updateReqVO);
    /**
     * åˆ é™¤çŸ¥è¯†åº“
     *
     * @param id çŸ¥è¯†åº“编号
     */
    void deleteKnowledge(Long id);
    /**
     * èŽ·å¾—çŸ¥è¯†åº“
     *
     * @param id ç¼–号
     * @return çŸ¥è¯†åº“
     */
    AiKnowledgeDO getKnowledge(Long id);
    /**
     * æ ¡éªŒçŸ¥è¯†åº“是否存在
     *
     * @param id è®°å½•编号
     */
    AiKnowledgeDO validateKnowledgeExists(Long id);
    /**
     * èŽ·å¾—çŸ¥è¯†åº“åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return çŸ¥è¯†åº“分页
     */
    AiKnowledgeDO validateKnowledge(Long id);
    PageResult<AiKnowledgeDO> getKnowledgePage(AiKnowledgePageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‡å®šçŠ¶æ€çš„çŸ¥è¯†åº“åˆ—è¡¨
     *
     * @param status çŠ¶æ€
     * @return çŸ¥è¯†åº“列表
     */
    List<AiKnowledgeDO> getKnowledgeSimpleListByStatus(Integer status);
    List<AiKnowledgeDO> getEnabledKnowledgeList();
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java
@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
@@ -8,102 +8,83 @@
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper;
import cn.iocoder.yudao.module.ai.enums.model.AiModelTypeEnum;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
/**
 * AI çŸ¥è¯†åº“-基础信息 Service å®žçŽ°ç±»
 *
 * @author xiaoxin
 */
@Service
@Slf4j
public class AiKnowledgeServiceImpl implements AiKnowledgeService {
    @Resource
    private AiKnowledgeMapper knowledgeMapper;
    private AiKnowledgeMapper mapper;
    @Resource
    private AiModelService modelService;
    @Resource
    private AiKnowledgeSegmentService knowledgeSegmentService;
    @Resource
    private AiKnowledgeDocumentService knowledgeDocumentService;
    @Override
    public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) {
        // 1. æ ¡éªŒæ¨¡åž‹é…ç½®
        AiModelDO model = modelService.validateModel(createReqVO.getEmbeddingModelId());
        // 2. æ’入知识库
        AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
                .setEmbeddingModel(model.getModel());
        knowledgeMapper.insert(knowledge);
        AiModelDO embeddingModel = modelService.validateModel(createReqVO.getEmbeddingModelId());
        if (!AiModelTypeEnum.EMBEDDING.getType().equals(embeddingModel.getType())) {
            throw exception(MODEL_USE_TYPE_ERROR);
        }
        AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class);
        knowledge.setEmbeddingModel(embeddingModel.getModel());
        if (knowledge.getTopK() == null) knowledge.setTopK(3);
        if (knowledge.getSimilarityThreshold() == null) knowledge.setSimilarityThreshold(0.7);
        mapper.insert(knowledge);
        return knowledge.getId();
    }
    @Override
    public void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒçŸ¥è¯†åº“存在
        AiKnowledgeDO oldKnowledge = validateKnowledgeExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒæ¨¡åž‹é…ç½®
        AiModelDO model = modelService.validateModel(updateReqVO.getEmbeddingModelId());
        // 2. æ›´æ–°çŸ¥è¯†åº“
        AiKnowledgeDO updateObj = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class)
                .setEmbeddingModel(model.getModel());
        knowledgeMapper.updateById(updateObj);
        // 3. å¦‚果模型变化,需要 reindex æ‰€æœ‰çš„æ–‡æ¡£
        if (ObjUtil.notEqual(oldKnowledge.getEmbeddingModelId(), updateReqVO.getEmbeddingModelId())) {
            knowledgeSegmentService.reindexByKnowledgeIdAsync(updateReqVO.getId());
        validateKnowledgeExists(updateReqVO.getId());
        AiModelDO embeddingModel = modelService.validateModel(updateReqVO.getEmbeddingModelId());
        if (!AiModelTypeEnum.EMBEDDING.getType().equals(embeddingModel.getType())) {
            throw exception(MODEL_USE_TYPE_ERROR);
        }
        AiKnowledgeDO knowledge = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
        knowledge.setEmbeddingModel(embeddingModel.getModel());
        mapper.updateById(knowledge);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteKnowledge(Long id) {
        // 1. æ ¡éªŒå­˜åœ¨
        validateKnowledgeExists(id);
        // 2. åˆ é™¤çŸ¥è¯†åº“下的所有文档及段落
        knowledgeDocumentService.deleteKnowledgeDocumentByKnowledgeId(id);
        // 3. åˆ é™¤çŸ¥è¯†åº“
        // ç‰¹æ®Šï¼šçŸ¥è¯†åº“需要最后删除,不然相关的配置会找不到
        knowledgeMapper.deleteById(id);
        mapper.deleteById(id);
    }
    @Override
    public AiKnowledgeDO getKnowledge(Long id) {
        return knowledgeMapper.selectById(id);
        return mapper.selectById(id);
    }
    @Override
    public AiKnowledgeDO validateKnowledgeExists(Long id) {
        AiKnowledgeDO knowledge = knowledgeMapper.selectById(id);
        if (knowledge == null) {
            throw exception(KNOWLEDGE_NOT_EXISTS);
        AiKnowledgeDO knowledge = mapper.selectById(id);
        if (knowledge == null) throw exception(KNOWLEDGE_NOT_EXISTS);
        return knowledge;
        }
    @Override
    public AiKnowledgeDO validateKnowledge(Long id) {
        AiKnowledgeDO knowledge = validateKnowledgeExists(id);
        if (CommonStatusEnum.isDisable(knowledge.getStatus())) throw exception(KNOWLEDGE_NOT_EXISTS);
        return knowledge;
    }
    @Override
    public PageResult<AiKnowledgeDO> getKnowledgePage(AiKnowledgePageReqVO pageReqVO) {
        return knowledgeMapper.selectPage(pageReqVO);
        return mapper.selectPage(pageReqVO);
    }
    @Override
    public List<AiKnowledgeDO> getKnowledgeSimpleListByStatus(Integer status) {
        return knowledgeMapper.selectListByStatus(status);
    public List<AiKnowledgeDO> getEnabledKnowledgeList() {
        return mapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java
@@ -1,39 +1,20 @@
package cn.iocoder.yudao.module.ai.service.knowledge.bo;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import javax.validation.constraints.NotNull;
import jakarta.validation.constraints.NotEmpty;
/**
 * AI çŸ¥è¯†åº“段落搜索 Request BO
 *
 * @author èŠ‹é“æºç 
 */
@Data
public class AiKnowledgeSegmentSearchReqBO {
    /**
     * çŸ¥è¯†åº“编号
     */
    @NotNull(message = "知识库编号不能为空")
    private Long knowledgeId;
    /**
     * å†…容
     */
    @NotEmpty(message = "内容不能为空")
    private String content;
    /**
     * æœ€å¤§è¿”回数量
     */
    private Integer topK;
    /**
     * ç›¸ä¼¼åº¦é˜ˆå€¼
     */
    private Double similarityThreshold;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java
@@ -2,44 +2,23 @@
import lombok.Data;
/**
 * AI çŸ¥è¯†åº“段落搜索 Response BO
 *
 * @author èŠ‹é“æºç 
 */
@Data
public class AiKnowledgeSegmentSearchRespBO {
    /**
     * æ®µè½ç¼–号
     */
    private Long id;
    /**
     * æ–‡æ¡£ç¼–号
     */
    private Long documentId;
    /**
     * çŸ¥è¯†åº“编号
     */
    private String documentName;
    private Long knowledgeId;
    /**
     * å†…容
     */
    private String content;
    /**
     * å†…容长度
     */
    private Integer contentLength;
    /**
     * Token æ•°é‡
     */
    private Integer tokens;
    /**
     * ç›¸ä¼¼åº¦åˆ†æ•°
     */
    private Double score;
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/splitter/MarkdownQaSplitter.java
@@ -5,7 +5,6 @@
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -14,44 +13,16 @@
/**
 * Markdown QA æ ¼å¼ä¸“用切片器
 *
 * <p>功能特点:
 * <ul>
 *   <li>识别二级标题(## ï¼‰ä½œä¸ºé—®é¢˜æ ‡è®°</li>
 *   <li>短 QA å¯¹ä¿æŒå®Œæ•´ï¼ˆä¸è¶…过 Token é™åˆ¶ï¼‰</li>
 *   <li>长答案智能切分,每个片段保留完整问题作为上下文</li>
 *   <li>支持自定义 Token ä¼°ç®—器</li>
 * </ul>
 *
 * @author runzhen
 * è¯†åˆ«äºŒçº§æ ‡é¢˜ï¼ˆ## ï¼‰ä½œä¸ºé—®é¢˜æ ‡è®°ï¼Œä¿æŒé—®ç­”对完整性
 */
@Slf4j
@SuppressWarnings("SizeReplaceableByIsEmpty")
public class MarkdownQaSplitter extends TextSplitter {
    /**
     * äºŒçº§æ ‡é¢˜æ­£åˆ™ï¼šåŒ¹é… "## " å¼€å¤´çš„行
     */
    private static final Pattern H2_PATTERN = Pattern.compile("^##\\s+(.+)$", Pattern.MULTILINE);
    /**
     * æ®µè½åˆ†éš”符:双换行
     */
    private static final String PARAGRAPH_SEPARATOR = "\n\n";
    /**
     * å¥å­åˆ†éš”符
     */
    private static final Pattern SENTENCE_PATTERN = Pattern.compile("[。!?.!?]\\s*");
    /**
     * åˆ†æ®µçš„æœ€å¤§ Token æ•°
     */
    private final int chunkSize;
    /**
     * Token ä¼°ç®—器(简单实现:中文按字符数,英文按单词数的 1.3 å€ï¼‰
     */
    private final TokenEstimator tokenEstimator;
    public MarkdownQaSplitter(int chunkSize) {
@@ -61,282 +32,115 @@
    @Override
    protected List<String> splitText(String text) {
        if (StrUtil.isEmpty(text)) {
            return Collections.emptyList();
        }
        // è§£æž QA å¯¹
        if (StrUtil.isEmpty(text)) return Collections.emptyList();
        List<QaPair> qaPairs = parseQaPairs(text);
        if (CollUtil.isEmpty(qaPairs)) {
            // å¦‚果没有识别到 QA æ ¼å¼ï¼ŒæŒ‰æ®µè½åˆ‡åˆ†
            return fallbackSplit(text);
        }
        // å¤„理每个 QA å¯¹
        if (CollUtil.isEmpty(qaPairs)) return fallbackSplit(text);
        List<String> result = new ArrayList<>();
        for (QaPair qaPair : qaPairs) {
            result.addAll(splitQaPair(qaPair));
        }
        for (QaPair qaPair : qaPairs) result.addAll(splitQaPair(qaPair));
        return result;
    }
    /**
     * è§£æž Markdown QA å¯¹
     *
     * @param content æ–‡æœ¬å†…容
     * @return QA å¯¹åˆ—表
     */
    private List<QaPair> parseQaPairs(String content) {
        // æ‰¾åˆ°æ‰€æœ‰äºŒçº§æ ‡é¢˜ä½ç½®
        List<QaPair> qaPairs = new ArrayList<>();
        List<Integer> headingPositions = new ArrayList<>();
        List<String> questions = new ArrayList<>();
        Matcher matcher = H2_PATTERN.matcher(content);
        while (matcher.find()) {
            headingPositions.add(matcher.start());
            questions.add(matcher.group(1).trim());
        }
        if (CollUtil.isEmpty(headingPositions)) {
            return qaPairs;
        }
        // æå–每个 QA å¯¹
        while (matcher.find()) { headingPositions.add(matcher.start()); questions.add(matcher.group(1).trim()); }
        if (CollUtil.isEmpty(headingPositions)) return qaPairs;
        for (int i = 0; i < headingPositions.size(); i++) {
            int start = headingPositions.get(i);
            int end = (i + 1 < headingPositions.size())
                    ? headingPositions.get(i + 1)
                    : content.length();
            String qaText = content.substring(start, end).trim();
            int end = (i + 1 < headingPositions.size()) ? headingPositions.get(i + 1) : content.length();
            String fullText = content.substring(start, end).trim();
            String question = questions.get(i);
            // æå–答案部分(去掉问题标题)
            String answer = qaText.substring(qaText.indexOf('\n') + 1).trim();
            qaPairs.add(new QaPair(question, answer, qaText));
            String answer = fullText.substring(fullText.indexOf('\n') + 1).trim();
            qaPairs.add(new QaPair(question, answer, fullText));
        }
        return qaPairs;
    }
    /**
     * åˆ‡åˆ†å•个 QA å¯¹
     *
     * @param qaPair QA å¯¹
     * @return åˆ‡åˆ†åŽçš„æ–‡æœ¬ç‰‡æ®µåˆ—表
     */
    private List<String> splitQaPair(QaPair qaPair) {
        // å¦‚果整个 QA å¯¹ä¸è¶…过限制,保持完整
        int qaTokens = tokenEstimator.estimate(qaPair.fullText);
        if (qaTokens <= chunkSize) return Collections.singletonList(qaPair.fullText);
        List<String> chunks = new ArrayList<>();
        String fullQa = qaPair.fullText;
        int qaTokens = tokenEstimator.estimate(fullQa);
        if (qaTokens <= chunkSize) {
            chunks.add(fullQa);
            return chunks;
        }
        // é•¿ç­”案需要切分
        log.debug("QA å¯¹è¶…过 Token é™åˆ¶ ({} > {}),开始智能切分: {}", qaTokens, chunkSize, qaPair.question);
        List<String> answerChunks = splitLongAnswer(qaPair.answer, qaPair.question);
        for (String answerChunk : answerChunks) {
            // æ¯ä¸ªç‰‡æ®µéƒ½åŒ…含完整问题
            String chunkText = "## " + qaPair.question + "\n" + answerChunk;
            chunks.add(chunkText);
            chunks.add("## " + qaPair.question + "\n" + answerChunk);
        }
        return chunks;
    }
    /**
     * åˆ‡åˆ†é•¿ç­”案
     *
     * @param answer ç­”案文本
     * @param question é—®é¢˜æ–‡æœ¬
     * @return åˆ‡åˆ†åŽçš„答案片段列表
     */
    private List<String> splitLongAnswer(String answer, String question) {
        List<String> chunks = new ArrayList<>();
        // é¢„留问题的 Token ç©ºé—´
        String questionHeader = "## " + question + "\n";
        int questionTokens = tokenEstimator.estimate(questionHeader);
        int availableTokens = chunkSize - questionTokens - 10; // é¢„ç•™ 10 ä¸ª Token çš„缓冲
        // å…ˆæŒ‰æ®µè½åˆ‡åˆ†
        int availableTokens = chunkSize - questionTokens - 10;
        String[] paragraphs = answer.split(PARAGRAPH_SEPARATOR);
        StringBuilder currentChunk = new StringBuilder();
        int currentTokens = 0;
        for (String paragraph : paragraphs) {
            if (StrUtil.isEmpty(paragraph)) {
                continue;
            }
            if (StrUtil.isEmpty(paragraph)) continue;
            int paragraphTokens = tokenEstimator.estimate(paragraph);
            // å¦‚果单个段落就超过限制,需要按句子切分
            if (paragraphTokens > availableTokens) {
                // å…ˆä¿å­˜å½“前块
                if (currentChunk.length() > 0) {
                    chunks.add(currentChunk.toString().trim());
                    currentChunk = new StringBuilder();
                    currentTokens = 0;
                }
                // æŒ‰å¥å­åˆ‡åˆ†é•¿æ®µè½
                if (currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; }
                chunks.addAll(splitLongParagraph(paragraph, availableTokens));
                continue;
            }
            // å¦‚果加上这个段落会超过限制
            if (currentTokens + paragraphTokens > availableTokens && currentChunk.length() > 0) {
                chunks.add(currentChunk.toString().trim());
                currentChunk = new StringBuilder();
                currentTokens = 0;
            }
            if (currentChunk.length() > 0) {
                currentChunk.append("\n\n");
            }
            // æ·»åŠ æ®µè½
            if (currentTokens + paragraphTokens > availableTokens && currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; }
            if (currentChunk.length() > 0) currentChunk.append("\n\n");
            currentChunk.append(paragraph);
            currentTokens += paragraphTokens;
        }
        // æ·»åŠ æœ€åŽä¸€å—
        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString().trim());
        }
        if (currentChunk.length() > 0) chunks.add(currentChunk.toString().trim());
        return CollUtil.isEmpty(chunks) ? Collections.singletonList(answer) : chunks;
    }
    /**
     * åˆ‡åˆ†é•¿æ®µè½ï¼ˆæŒ‰å¥å­ï¼‰
     *
     * @param paragraph æ®µè½æ–‡æœ¬
     * @param availableTokens å¯ç”¨çš„ Token æ•°
     * @return åˆ‡åˆ†åŽçš„æ–‡æœ¬ç‰‡æ®µåˆ—表
     */
    private List<String> splitLongParagraph(String paragraph, int availableTokens) {
        // æŒ‰å¥å­åˆ‡åˆ†
        List<String> chunks = new ArrayList<>();
        String[] sentences = SENTENCE_PATTERN.split(paragraph);
        // æŒ‰å¥å­ç´¯ç§¯åˆ‡åˆ†
        StringBuilder currentChunk = new StringBuilder();
        int currentTokens = 0;
        for (String sentence : sentences) {
            if (StrUtil.isEmpty(sentence)) {
                continue;
            }
            if (StrUtil.isEmpty(sentence)) continue;
            int sentenceTokens = tokenEstimator.estimate(sentence);
            // å¦‚果单个句子就超过限制,强制切分
            if (sentenceTokens > availableTokens) {
                if (currentChunk.length() > 0) {
                    chunks.add(currentChunk.toString().trim());
                    currentChunk = new StringBuilder();
                    currentTokens = 0;
                }
                chunks.add(sentence.trim());
                continue;
            }
            // å¦‚果加上这个句子会超过限制
            if (currentTokens + sentenceTokens > availableTokens && currentChunk.length() > 0) {
                chunks.add(currentChunk.toString().trim());
                currentChunk = new StringBuilder();
                currentTokens = 0;
            }
            // æ·»åŠ å¥å­
            if (sentenceTokens > availableTokens) { if (currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; } chunks.add(sentence.trim()); continue; }
            if (currentTokens + sentenceTokens > availableTokens && currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; }
            currentChunk.append(sentence);
            currentTokens += sentenceTokens;
        }
        // æ·»åŠ æœ€åŽä¸€å—
        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString().trim());
        }
        if (currentChunk.length() > 0) chunks.add(currentChunk.toString().trim());
        return chunks.isEmpty() ? Collections.singletonList(paragraph) : chunks;
    }
    /**
     * é™çº§åˆ‡åˆ†ç­–略(当未识别到 QA æ ¼å¼æ—¶ï¼‰
     *
     * @param content æ–‡æœ¬å†…容
     * @return åˆ‡åˆ†åŽçš„æ–‡æœ¬ç‰‡æ®µåˆ—表
     */
    private List<String> fallbackSplit(String content) {
        // æŒ‰æ®µè½åˆ‡åˆ†
        List<String> chunks = new ArrayList<>();
        String[] paragraphs = content.split(PARAGRAPH_SEPARATOR);
        // æŒ‰æ®µè½ç´¯ç§¯åˆ‡åˆ†
        StringBuilder currentChunk = new StringBuilder();
        int currentTokens = 0;
        for (String paragraph : paragraphs) {
            if (StrUtil.isEmpty(paragraph)) {
                continue;
            }
            if (StrUtil.isEmpty(paragraph)) continue;
            int paragraphTokens = tokenEstimator.estimate(paragraph);
            // å¦‚果加上这个段落会超过限制
            if (currentTokens + paragraphTokens > chunkSize && currentChunk.length() > 0) {
                chunks.add(currentChunk.toString().trim());
                currentChunk = new StringBuilder();
                currentTokens = 0;
            }
            // æ·»åŠ æ®µè½
            if (currentChunk.length() > 0) {
                currentChunk.append("\n\n");
            }
            if (currentTokens + paragraphTokens > chunkSize && currentChunk.length() > 0) { chunks.add(currentChunk.toString().trim()); currentChunk = new StringBuilder(); currentTokens = 0; }
            if (currentChunk.length() > 0) currentChunk.append("\n\n");
            currentChunk.append(paragraph);
            currentTokens += paragraphTokens;
        }
        // æ·»åŠ æœ€åŽä¸€å—
        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString().trim());
        }
        if (currentChunk.length() > 0) chunks.add(currentChunk.toString().trim());
        return chunks.isEmpty() ? Collections.singletonList(content) : chunks;
    }
    /**
     * QA å¯¹æ•°æ®ç»“æž„
     */
    @AllArgsConstructor
    private static class QaPair {
    private static class QaPair { String question; String answer; String fullText; }
        String question;
        String answer;
        String fullText;
    public interface TokenEstimator { int estimate(String text); }
    }
    /**
     * Token ä¼°ç®—器接口
     */
    public interface TokenEstimator {
        int estimate(String text);
    }
    /**
     * ç®€å•çš„ Token ä¼°ç®—器实现
     * ä¸­æ–‡ï¼š1 å­—符 â‰ˆ 1 Token
     * è‹±æ–‡ï¼š1 å•词 â‰ˆ 1.3 Token
     */
    private static class SimpleTokenEstimator implements TokenEstimator {
        @Override
        public int estimate(String text) {
            if (StrUtil.isEmpty(text)) {
                return 0;
            }
            int chineseChars = 0;
            int englishWords = 0;
            // ç®€å•统计中英文
            for (char c : text.toCharArray()) {
                if (c >= 0x4E00 && c <= 0x9FA5) {
                    chineseChars++;
                }
            }
            // è‹±æ–‡å•词估算
            String[] words = text.split("\\s+");
            for (String word : words) {
                if (word.matches(".*[a-zA-Z].*")) {
                    englishWords++;
                }
            }
            if (StrUtil.isEmpty(text)) return 0;
            int chineseChars = 0, englishWords = 0;
            for (char c : text.toCharArray()) { if (c >= 0x4E00 && c <= 0x9FA5) chineseChars++; }
            for (String word : text.split("\\s+")) { if (word.matches(".*[a-zA-Z].*")) englishWords++; }
            return chineseChars + (int) (englishWords * 1.3);
        }
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/splitter/SemanticTextSplitter.java
@@ -3,7 +3,6 @@
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -13,289 +12,117 @@
/**
 * è¯­ä¹‰åŒ–文本切片器
 *
 * <p>功能特点:
 * <ul>
 *   <li>优先在段落边界(双换行)处切分</li>
 *   <li>其次在句子边界(句号、问号、感叹号)处切分</li>
 *   <li>避免在句子中间截断,保持语义完整性</li>
 *   <li>支持中英文标点符号识别</li>
 * </ul>
 *
 * @author runzhen
 * ä¼˜å…ˆåœ¨æ®µè½è¾¹ç•Œå¤„切分,其次在句子边界处切分,保持语义完整性
 */
@Slf4j
public class SemanticTextSplitter extends TextSplitter {
    /**
     * åˆ†æ®µçš„æœ€å¤§ Token æ•°
     */
    private final int chunkSize;
    /**
     * æ®µè½é‡å å¤§å°ï¼ˆç”¨äºŽä¿æŒä¸Šä¸‹æ–‡è¿žè´¯æ€§ï¼‰
     */
    private final int chunkOverlap;
    /**
     * æ®µè½åˆ†éš”符(按优先级排序)
     */
    private static final List<String> PARAGRAPH_SEPARATORS = Arrays.asList(
            "\n\n\n",    // ä¸‰ä¸ªæ¢è¡Œ
            "\n\n",      // åŒæ¢è¡Œ
            "\n"         // å•换行
    );
    /**
     * å¥å­ç»“束标记(中英文标点)
     */
    private static final Pattern SENTENCE_END_PATTERN = Pattern.compile(
            "[。!?.!?]+[\\s\"'))】\\]]*"
    );
    /**
     * Token ä¼°ç®—器
     */
    private static final List<String> PARAGRAPH_SEPARATORS = Arrays.asList("\n\n\n", "\n\n", "\n");
    private static final Pattern SENTENCE_END_PATTERN = Pattern.compile("[。!?.!?]+[\\s\"'))】\\]]*");
    private final MarkdownQaSplitter.TokenEstimator tokenEstimator;
    public SemanticTextSplitter(int chunkSize, int chunkOverlap) {
        this.chunkSize = chunkSize;
        this.chunkOverlap = Math.min(chunkOverlap, chunkSize / 2); // é‡å ä¸è¶…过一半
        this.chunkOverlap = Math.min(chunkOverlap, chunkSize / 2);
        this.tokenEstimator = new SimpleTokenEstimator();
    }
    public SemanticTextSplitter(int chunkSize) {
        this(chunkSize, 50); // é»˜è®¤é‡å  50 ä¸ª Token
    }
    public SemanticTextSplitter(int chunkSize) { this(chunkSize, 50); }
    @Override
    protected List<String> splitText(String text) {
        if (StrUtil.isEmpty(text)) {
            return Collections.emptyList();
        }
        if (StrUtil.isEmpty(text)) return Collections.emptyList();
        return splitTextRecursive(text);
    }
    /**
     * åˆ‡åˆ†æ–‡æœ¬ï¼ˆé€’归策略)
     *
     * @param text å¾…切分文本
     * @return åˆ‡åˆ†åŽçš„æ–‡æœ¬å—列表
     */
    private List<String> splitTextRecursive(String text) {
        List<String> chunks = new ArrayList<>();
        // å¦‚果文本不超过限制,直接返回
        int textTokens = tokenEstimator.estimate(text);
        if (textTokens <= chunkSize) {
            chunks.add(text.trim());
            return chunks;
        }
        if (textTokens <= chunkSize) return Collections.singletonList(text.trim());
        // å°è¯•按不同分隔符切分
        List<String> splits = null;
        String usedSeparator = null;
        for (String separator : PARAGRAPH_SEPARATORS) {
            if (text.contains(separator)) {
                splits = Arrays.asList(text.split(Pattern.quote(separator)));
                usedSeparator = separator;
                break;
            if (text.contains(separator)) { splits = Arrays.asList(text.split(Pattern.quote(separator))); usedSeparator = separator; break; }
            }
        if (splits == null || splits.size() == 1) { splits = splitBySentences(text); usedSeparator = ""; }
        return mergeSplits(splits, usedSeparator);
        }
        // å¦‚果没有找到段落分隔符,按句子切分
        if (splits == null || splits.size() == 1) {
            splits = splitBySentences(text);
            usedSeparator = ""; // å¥å­åˆ‡åˆ†ä¸éœ€è¦åˆ†éš”符
        }
        // åˆå¹¶å°ç‰‡æ®µ
        chunks = mergeSplits(splits, usedSeparator);
        return chunks;
    }
    /**
     * æŒ‰å¥å­åˆ‡åˆ†
     *
     * @param text å¾…切分文本
     * @return å¥å­åˆ—表
     */
    private List<String> splitBySentences(String text) {
        // ä½¿ç”¨æ­£åˆ™è¡¨è¾¾å¼åŒ¹é…å¥å­ç»“束位置
        List<String> sentences = new ArrayList<>();
        int lastEnd = 0;
        Matcher matcher = SENTENCE_END_PATTERN.matcher(text);
        while (matcher.find()) {
            String sentence = text.substring(lastEnd, matcher.end()).trim();
            if (StrUtil.isNotEmpty(sentence)) {
                sentences.add(sentence);
            }
            if (StrUtil.isNotEmpty(sentence)) sentences.add(sentence);
            lastEnd = matcher.end();
        }
        // æ·»åŠ å‰©ä½™éƒ¨åˆ†
        if (lastEnd < text.length()) {
            String remaining = text.substring(lastEnd).trim();
            if (StrUtil.isNotEmpty(remaining)) {
                sentences.add(remaining);
            }
        }
        if (lastEnd < text.length()) { String remaining = text.substring(lastEnd).trim(); if (StrUtil.isNotEmpty(remaining)) sentences.add(remaining); }
        return sentences.isEmpty() ? Collections.singletonList(text) : sentences;
    }
    /**
     * åˆå¹¶åˆ‡åˆ†åŽçš„小片段
     *
     * @param splits åˆ‡åˆ†åŽçš„片段列表
     * @param separator ç‰‡æ®µé—´çš„分隔符
     * @return åˆå¹¶åŽçš„æ–‡æœ¬å—列表
     */
    private List<String> mergeSplits(List<String> splits, String separator) {
        List<String> chunks = new ArrayList<>();
        List<String> currentChunks = new ArrayList<>();
        int currentLength = 0;
        for (String split : splits) {
            if (StrUtil.isEmpty(split)) {
                continue;
            }
            if (StrUtil.isEmpty(split)) continue;
            int splitTokens = tokenEstimator.estimate(split);
            // å¦‚果单个片段就超过限制,进一步递归切分
            if (splitTokens > chunkSize) {
                // å…ˆä¿å­˜å½“前累积的块
                if (!currentChunks.isEmpty()) {
                    String chunkText = String.join(separator, currentChunks);
                    chunks.add(chunkText.trim());
                    currentChunks.clear();
                    currentLength = 0;
                }
                // é€’归切分大片段
                if (!separator.isEmpty()) {
                    // å¦‚果是段落分隔符,尝试按句子切分
                    chunks.addAll(splitTextRecursive(split));
                } else {
                    // å¦‚果已经是句子级别,强制按字符切分
                    chunks.addAll(forceSplitLongText(split));
                }
                if (!currentChunks.isEmpty()) { chunks.add(String.join(separator, currentChunks).trim()); currentChunks.clear(); currentLength = 0; }
                if (!separator.isEmpty()) { chunks.addAll(splitTextRecursive(split)); } else { chunks.addAll(forceSplitLongText(split)); }
                continue;
            }
            // è®¡ç®—加上分隔符的 Token æ•°
            int separatorTokens = StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator);
            // å¦‚果加上这个片段会超过限制
            if (!currentChunks.isEmpty() && currentLength + splitTokens + separatorTokens > chunkSize) {
                // ä¿å­˜å½“前块
                String chunkText = String.join(separator, currentChunks);
                chunks.add(chunkText.trim());
                // å¤„理重叠:保留最后几个片段
                chunks.add(String.join(separator, currentChunks).trim());
                currentChunks = getOverlappingChunks(currentChunks, separator);
                currentLength = estimateTokens(currentChunks, separator);
            }
            // æ·»åŠ å½“å‰ç‰‡æ®µ
            currentChunks.add(split);
            currentLength += splitTokens + separatorTokens;
        }
        // æ·»åŠ æœ€åŽä¸€å—
        if (!currentChunks.isEmpty()) {
            String chunkText = String.join(separator, currentChunks);
            chunks.add(chunkText.trim());
        }
        if (!currentChunks.isEmpty()) chunks.add(String.join(separator, currentChunks).trim());
        return chunks;
    }
    /**
     * èŽ·å–é‡å çš„ç‰‡æ®µï¼ˆç”¨äºŽä¿æŒä¸Šä¸‹æ–‡ï¼‰
     *
     * @param chunks å½“前片段列表
     * @param separator ç‰‡æ®µé—´çš„分隔符
     * @return é‡å çš„片段列表
     */
    private List<String> getOverlappingChunks(List<String> chunks, String separator) {
        if (chunkOverlap == 0 || chunks.isEmpty()) {
            return new ArrayList<>();
        }
        // ä»ŽåŽå¾€å‰å–片段,直到达到重叠大小
        if (chunkOverlap == 0 || chunks.isEmpty()) return new ArrayList<>();
        List<String> overlapping = new ArrayList<>();
        int tokens = 0;
        for (int i = chunks.size() - 1; i >= 0; i--) {
            String chunk = chunks.get(i);
            int chunkTokens = tokenEstimator.estimate(chunk);
            if (tokens + chunkTokens > chunkOverlap) {
                break;
            }
            // æ·»åŠ åˆ°é‡å åˆ—è¡¨å‰ç«¯
            if (tokens + chunkTokens > chunkOverlap) break;
            overlapping.add(0, chunk);
            tokens += chunkTokens + (StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator));
        }
        return overlapping;
    }
    /**
     * ä¼°ç®—片段列表的总 Token æ•°
     *
     * @param chunks ç‰‡æ®µåˆ—表
     * @param separator ç‰‡æ®µé—´çš„分隔符
     * @return æ€» Token æ•°
     */
    private int estimateTokens(List<String> chunks, String separator) {
        int total = 0;
        for (int i = 0; i < chunks.size(); i++) {
            total += tokenEstimator.estimate(chunks.get(i));
            if (i < chunks.size() - 1 && StrUtil.isNotEmpty(separator)) {
                total += tokenEstimator.estimate(separator);
            }
        }
        for (int i = 0; i < chunks.size(); i++) { total += tokenEstimator.estimate(chunks.get(i)); if (i < chunks.size() - 1 && StrUtil.isNotEmpty(separator)) total += tokenEstimator.estimate(separator); }
        return total;
    }
    /**
     * å¼ºåˆ¶åˆ‡åˆ†é•¿æ–‡æœ¬ï¼ˆå½“语义切分失败时)
     *
     * @param text å¾…切分文本
     * @return åˆ‡åˆ†åŽçš„æ–‡æœ¬å—列表
     */
    private List<String> forceSplitLongText(String text) {
        List<String> chunks = new ArrayList<>();
        int charsPerChunk = (int) (chunkSize * 0.8); // ä¿å®ˆä¼°è®¡
        for (int i = 0; i < text.length(); i += charsPerChunk) {
            int end = Math.min(i + charsPerChunk, text.length());
            String chunk = text.substring(i, end);
            chunks.add(chunk.trim());
        }
        log.warn("文本过长,已强制按字符切分,可能影响语义完整性");
        int charsPerChunk = (int)(chunkSize * 0.8);
        for (int i = 0; i < text.length(); i += charsPerChunk) { int end = Math.min(i + charsPerChunk, text.length()); chunks.add(text.substring(i, end).trim()); }
        return chunks;
    }
    /**
     * ç®€å•çš„ Token ä¼°ç®—器实现
     */
    private static class SimpleTokenEstimator implements MarkdownQaSplitter.TokenEstimator {
        @Override
        public int estimate(String text) {
            if (StrUtil.isEmpty(text)) {
                return 0;
            }
            int chineseChars = 0;
            int englishWords = 0;
            // ç®€å•统计中英文
            for (char c : text.toCharArray()) {
                if (c >= 0x4E00 && c <= 0x9FA5) {
                    chineseChars++;
                }
            }
            // è‹±æ–‡å•词估算
            String[] words = text.split("\\s+");
            for (String word : words) {
                if (word.matches(".*[a-zA-Z].*")) {
                    englishWords++;
                }
            }
            if (StrUtil.isEmpty(text)) return 0;
            int chineseChars = 0, englishWords = 0;
            for (char c : text.toCharArray()) { if (c >= 0x4E00 && c <= 0x9FA5) chineseChars++; }
            for (String word : text.split("\\s+")) { if (word.matches(".*[a-zA-Z].*")) englishWords++; }
            return chineseChars + (int) (englishWords * 1.3);
        }
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
@@ -4,77 +4,21 @@
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * AI API å¯†é’¥ Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface AiApiKeyService {
    /**
     * åˆ›å»º API å¯†é’¥
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createApiKey(@Valid AiApiKeySaveReqVO createReqVO);
    Long createApiKey(AiApiKeySaveReqVO createReqVO);
    /**
     * æ›´æ–° API å¯†é’¥
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateApiKey(@Valid AiApiKeySaveReqVO updateReqVO);
    void updateApiKey(AiApiKeySaveReqVO updateReqVO);
    /**
     * åˆ é™¤ API å¯†é’¥
     *
     * @param id ç¼–号
     */
    void deleteApiKey(Long id);
    /**
     * èŽ·å¾— API å¯†é’¥
     *
     * @param id ç¼–号
     * @return API å¯†é’¥
     */
    AiApiKeyDO getApiKey(Long id);
    /**
     * æ ¡éªŒ API å¯†é’¥
     *
     * @param id æ¯”那好
     * @return API å¯†é’¥
     */
    AiApiKeyDO validateApiKey(Long id);
    /**
     * èŽ·å¾— API å¯†é’¥åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return API å¯†é’¥åˆ†é¡µ
     */
    PageResult<AiApiKeyDO> getApiKeyPage(AiApiKeyPageReqVO pageReqVO);
    /**
     * èŽ·å¾— API å¯†é’¥åˆ—表
     *
     * @return API å¯†é’¥åˆ—表
     */
    List<AiApiKeyDO> getApiKeyList();
    /**
     * èŽ·å¾—é»˜è®¤çš„ API å¯†é’¥
     *
     * @param platform å¹³å°
     * @param status çŠ¶æ€
     * @return API å¯†é’¥
     */
    AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status);
    PageResult<AiApiKeyDO> getApiKeyPage(AiApiKeyPageReqVO pageReqVO);
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
@@ -9,91 +9,59 @@
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_KEY_DISABLE;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
/**
 * AI API å¯†é’¥ Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class AiApiKeyServiceImpl implements AiApiKeyService {
    @Resource
    private AiApiKeyMapper apiKeyMapper;
    private AiApiKeyMapper mapper;
    @Override
    public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
        // æ’å…¥
        AiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);
        apiKeyMapper.insert(apiKey);
        // è¿”回
        return apiKey.getId();
        AiApiKeyDO key = BeanUtils.toBean(createReqVO, AiApiKeyDO.class);
        mapper.insert(key);
        return key.getId();
    }
    @Override
    public void updateApiKey(AiApiKeySaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateApiKeyExists(updateReqVO.getId());
        // æ›´æ–°
        AiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class);
        apiKeyMapper.updateById(updateObj);
        mapper.updateById(BeanUtils.toBean(updateReqVO, AiApiKeyDO.class));
    }
    @Override
    public void deleteApiKey(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateApiKeyExists(id);
        // åˆ é™¤
        apiKeyMapper.deleteById(id);
    }
    private AiApiKeyDO validateApiKeyExists(Long id) {
        AiApiKeyDO apiKey = apiKeyMapper.selectById(id);
        if (apiKey == null) {
            throw exception(API_KEY_NOT_EXISTS);
        }
        return apiKey;
        mapper.deleteById(id);
    }
    @Override
    public AiApiKeyDO getApiKey(Long id) {
        return apiKeyMapper.selectById(id);
        return mapper.selectById(id);
    }
    @Override
    public AiApiKeyDO validateApiKey(Long id) {
        AiApiKeyDO apiKey = validateApiKeyExists(id);
        if (CommonStatusEnum.isDisable(apiKey.getStatus())) {
            throw exception(API_KEY_DISABLE);
        }
        return apiKey;
    }
    @Override
    public PageResult<AiApiKeyDO> getApiKeyPage(AiApiKeyPageReqVO pageReqVO) {
        return apiKeyMapper.selectPage(pageReqVO);
    }
    @Override
    public List<AiApiKeyDO> getApiKeyList() {
        return apiKeyMapper.selectList();
        AiApiKeyDO key = mapper.selectById(id);
        if (key == null) throw exception(API_KEY_NOT_EXISTS);
        if (CommonStatusEnum.isDisable(key.getStatus())) throw exception(API_KEY_DISABLE);
        return key;
    }
    @Override
    public AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) {
        AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status);
        if (apiKey == null) {
            throw exception(API_KEY_NOT_EXISTS);
        return mapper.selectFirstByPlatformAndStatus(platform, status);
        }
        return apiKey;
    @Override
    public PageResult<AiApiKeyDO> getApiKeyPage(AiApiKeyPageReqVO pageReqVO) {
        return mapper.selectPage(pageReqVO);
    }
    private void validateApiKeyExists(Long id) {
        if (mapper.selectById(id) == null) throw exception(API_KEY_NOT_EXISTS);
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java
@@ -1,143 +1,36 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import dev.tinyflow.core.Tinyflow;
import jakarta.validation.Valid;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.vectorstore.VectorStore;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
/**
 * AI æ¨¡åž‹ Service æŽ¥å£
 *
 * @author fansili
 * @since 2024/4/24 19:42
 */
public interface AiModelService {
    /**
     * åˆ›å»ºæ¨¡åž‹
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createModel(@Valid AiModelSaveReqVO createReqVO);
    /**
     * æ›´æ–°æ¨¡åž‹
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateModel(@Valid AiModelSaveReqVO updateReqVO);
    /**
     * åˆ é™¤æ¨¡åž‹
     *
     * @param id ç¼–号
     */
    void deleteModel(Long id);
    /**
     * èŽ·å¾—æ¨¡åž‹
     *
     * @param id ç¼–号
     * @return æ¨¡åž‹
     */
    AiModelDO getModel(Long id);
    /**
     * èŽ·å¾—é»˜è®¤çš„æ¨¡åž‹
     *
     * å¦‚果获取不到,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} ä¸šåС异叏
     *
     * @return æ¨¡åž‹
     */
    AiModelDO getRequiredDefaultModel(Integer type);
    /**
     * èŽ·å¾—æ¨¡åž‹åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return æ¨¡åž‹åˆ†é¡µ
     */
    PageResult<AiModelDO> getModelPage(AiModelPageReqVO pageReqVO);
    /**
     * æ ¡éªŒæ¨¡åž‹æ˜¯å¦å¯ä½¿ç”¨
     *
     * @param id ç¼–号
     * @return æ¨¡åž‹
     */
    AiModelDO validateModel(Long id);
    /**
     * èŽ·å¾—æ¨¡åž‹åˆ—è¡¨
     *
     * @param status çŠ¶æ€
     * @param type ç±»åž‹
     * @param platform å¹³å°ï¼Œå…è®¸ç©º
     * @return æ¨¡åž‹åˆ—表
     */
    List<AiModelDO> getModelListByStatusAndType(Integer status, Integer type,
                                                @Nullable String platform);
    List<AiModelDO> getModelListByStatusAndType(Integer status, Integer type, @Nullable String platform);
    // ========== ä¸Ž Spring AI é›†æˆ ==========
    /**
     * èŽ·å¾— ChatModel å¯¹è±¡
     *
     * @param id ç¼–号
     * @return ChatModel å¯¹è±¡
     */
    ChatModel getChatModel(Long id);
    /**
     * èŽ·å¾— ImageModel å¯¹è±¡
     *
     * @param id ç¼–号
     * @return ImageModel å¯¹è±¡
     */
    ImageModel getImageModel(Long id);
    /**
     * èŽ·å¾— MidjourneyApi å¯¹è±¡
     *
     * @param id ç¼–号
     * @return MidjourneyApi å¯¹è±¡
     */
    MidjourneyApi getMidjourneyApi(Long id);
    /**
     * èŽ·å¾— SunoApi å¯¹è±¡
     *
     * @return SunoApi å¯¹è±¡
     */
    SunoApi getSunoApi();
    /**
     * èŽ·å¾— VectorStore å¯¹è±¡
     *
     * @param id ç¼–号
     * @param metadataFields å…ƒæ•°æ®çš„定义
     * @return VectorStore å¯¹è±¡
     */
    VectorStore getOrCreateVectorStore(Long id, Map<String, Class<?>> metadataFields);
    /**
     * èŽ·å– TinyFlow æ‰€éœ€ LLm Provider
     *
     * @param tinyflow tinyflow
     * @param modelId AI æ¨¡åž‹ ID
     */
    void getLLmProvider4Tinyflow(Tinyflow tinyflow, Long modelId);
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
@@ -1,9 +1,5 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -11,52 +7,36 @@
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatMapper;
import com.agentsflex.llm.ollama.OllamaLlm;
import com.agentsflex.llm.ollama.OllamaLlmConfig;
import com.agentsflex.llm.qwen.QwenLlm;
import com.agentsflex.llm.qwen.QwenLlmConfig;
import dev.tinyflow.core.Tinyflow;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiModelMapper;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
/**
 * AI æ¨¡åž‹ Service å®žçŽ°ç±»
 *
 * @author fansili
 */
@Service
@Validated
public class AiModelServiceImpl implements AiModelService {
    @Resource
    private AiApiKeyService apiKeyService;
    @Resource
    private AiChatMapper modelMapper;
    private AiModelMapper modelMapper;
    @Resource
    private AiModelFactory modelFactory;
    @Override
    public Long createModel(AiModelSaveReqVO createReqVO) {
        // 1. æ ¡éªŒ
        AiPlatformEnum.validatePlatform(createReqVO.getPlatform());
        apiKeyService.validateApiKey(createReqVO.getKeyId());
        // 2. æ’å…¥
        AiModelDO model = BeanUtils.toBean(createReqVO, AiModelDO.class);
        modelMapper.insert(model);
        return model.getId();
@@ -64,30 +44,16 @@
    @Override
    public void updateModel(AiModelSaveReqVO updateReqVO) {
        // 1. æ ¡éªŒ
        validateModelExists(updateReqVO.getId());
        AiPlatformEnum.validatePlatform(updateReqVO.getPlatform());
        apiKeyService.validateApiKey(updateReqVO.getKeyId());
        // 2. æ›´æ–°
        AiModelDO updateObj = BeanUtils.toBean(updateReqVO, AiModelDO.class);
        modelMapper.updateById(updateObj);
        modelMapper.updateById(BeanUtils.toBean(updateReqVO, AiModelDO.class));
    }
    @Override
    public void deleteModel(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateModelExists(id);
        // åˆ é™¤
        modelMapper.deleteById(id);
    }
    private AiModelDO validateModelExists(Long id) {
        AiModelDO model = modelMapper.selectById(id);
        if (modelMapper.selectById(id) == null) {
            throw exception(MODEL_NOT_EXISTS);
        }
        return model;
    }
    @Override
@@ -98,9 +64,7 @@
    @Override
    public AiModelDO getRequiredDefaultModel(Integer type) {
        AiModelDO model = modelMapper.selectFirstByStatus(type, CommonStatusEnum.ENABLE.getStatus());
        if (model == null) {
            throw exception(MODEL_DEFAULT_NOT_EXISTS);
        }
        if (model == null) throw exception(MODEL_DISABLE);
        return model;
    }
@@ -112,15 +76,19 @@
    @Override
    public AiModelDO validateModel(Long id) {
        AiModelDO model = validateModelExists(id);
        if (CommonStatusEnum.isDisable(model.getStatus())) {
            throw exception(MODEL_DISABLE);
        }
        if (CommonStatusEnum.isDisable(model.getStatus())) throw exception(MODEL_DISABLE);
        return model;
    }
    @Override
    public List<AiModelDO> getModelListByStatusAndType(Integer status, Integer type, String platform) {
        return modelMapper.selectListByStatusAndType(status, type, platform);
    }
    private AiModelDO validateModelExists(Long id) {
        AiModelDO model = modelMapper.selectById(id);
        if (model == null) throw exception(MODEL_NOT_EXISTS);
        return model;
    }
    // ========== ä¸Ž Spring AI é›†æˆ ==========
@@ -130,72 +98,19 @@
        AiModelDO model = validateModel(id);
        AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId());
        AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
        return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl());
    }
    @Override
    public ImageModel getImageModel(Long id) {
        AiModelDO model = validateModel(id);
        AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId());
        AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
        return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl());
    }
    @Override
    public MidjourneyApi getMidjourneyApi(Long id) {
        AiModelDO model = validateModel(id);
        AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId());
        return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
    }
    @Override
    public SunoApi getSunoApi() {
        AiApiKeyDO apiKey = apiKeyService.getRequiredDefaultApiKey(
                AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
        return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
        return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel());
    }
    @Override
    public VectorStore getOrCreateVectorStore(Long id, Map<String, Class<?>> metadataFields) {
        // èŽ·å–æ¨¡åž‹ + å¯†é’¥
        AiModelDO model = validateModel(id);
        AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId());
        AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
        // åˆ›å»ºæˆ–获取 EmbeddingModel å¯¹è±¡
        EmbeddingModel embeddingModel = modelFactory.getOrCreateEmbeddingModel(
                platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel());
        // åˆ›å»ºæˆ–获取 VectorStore å¯¹è±¡
         return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields);
//         return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields);
//         return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields);
//         return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields);
    }
    // TODO @lesan:是不是返回 Llm å¯¹è±¡ä¼šå¥½ç‚¹å“ˆï¼Ÿ
    @Override
    public void getLLmProvider4Tinyflow(Tinyflow tinyflow, Long modelId) {
        AiModelDO model = validateModel(modelId);
        AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId());
        AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
        switch (platform) {
            // TODO @lesan è€ƒè™‘到未来不需要使用agents-flex çŽ°åœ¨ä»…æµ‹è¯•é€šä¹‰åƒé—®
            // TODO @lesan:【重要】是不是可以实现一个 SpringAiLlm,这样的话,内部全部用它就好了。只实现 chat éƒ¨åˆ†ï¼›è¿™æ ·ï¼Œå°±æŠŠ flex ä½œä¸ºä¸€ä¸ª agent æ¡†æž¶ï¼Œå†…部调用,还是 spring ai ç›¸å…³çš„。成本可能低一点?!
            case TONG_YI:
                QwenLlmConfig qwenLlmConfig = new QwenLlmConfig();
                qwenLlmConfig.setApiKey(apiKey.getApiKey());
                qwenLlmConfig.setModel(model.getModel());
                // TODO @lesan:这个有点奇怪。。。如果一个链式里,有多个模型,咋整呀。。。
                tinyflow.setLlmProvider(id -> new QwenLlm(qwenLlmConfig));
                break;
            case OLLAMA:
                OllamaLlmConfig ollamaLlmConfig = new OllamaLlmConfig();
                ollamaLlmConfig.setEndpoint(apiKey.getUrl());
                ollamaLlmConfig.setModel(model.getModel());
                tinyflow.setLlmProvider(id -> new OllamaLlm(ollamaLlmConfig));
                break;
        }
        return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields);
    }
}
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/FileTypeUtils.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/org/springframework/ai/model/tool/DefaultToolExecutionEligibilityPredicate.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/main/java/org/springframework/ai/model/tool/ToolExecutionEligibilityPredicate.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/CozeChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DifyChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/FastGPTChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GrokChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/StepFunChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/MidjourneyApiTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/SiliconFlowImageModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/TongYiImagesModelTest.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/mcp/DouBaoMcpTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/music/SunoApiTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/ppt/wdd/WenDuoDuoPptApiTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/ppt/xunfei/XunFeiPptApiTests.java
ÎļþÒÑɾ³ý
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java
ÎļþÒÑɾ³ý
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/BpmOANoticeController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package cn.iocoder.yudao.module.bpm.controller.admin.oa;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticePageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticeRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticeSaveReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeDO;
import cn.iocoder.yudao.module.bpm.service.oa.OaNoticeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - OA é€šçŸ¥å…¬å‘Š")
@RestController
@RequestMapping("/bpm/oa/notice")
@Validated
public class BpmOANoticeController {
    @Resource
    private OaNoticeService oaNoticeService;
    @PostMapping("/create")
    @Operation(summary = "创建通知公告")
    @PreAuthorize("@ss.hasPermission('bpm:oa-notice:create')")
    public CommonResult<Long> createNotice(@Valid @RequestBody OaNoticeSaveReqVO createReqVO) {
        return success(oaNoticeService.createNotice(getLoginUserId(), createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改通知公告")
    @PreAuthorize("@ss.hasPermission('bpm:oa-notice:update')")
    public CommonResult<Boolean> updateNotice(@Valid @RequestBody OaNoticeSaveReqVO updateReqVO) {
        oaNoticeService.updateNotice(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除通知公告")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('bpm:oa-notice:delete')")
    public CommonResult<Boolean> deleteNotice(@RequestParam("id") Long id) {
        oaNoticeService.deleteNotice(id);
        return success(true);
    }
    @GetMapping("/page")
    @Operation(summary = "获取通知公告分页列表")
    @PreAuthorize("@ss.hasPermission('bpm:oa-notice:query')")
    public CommonResult<PageResult<OaNoticeRespVO>> getNoticePage(@Valid OaNoticePageReqVO pageReqVO) {
        PageResult<OaNoticeDO> pageResult = oaNoticeService.getNoticePage(getLoginUserId(), pageReqVO);
        return success(BeanUtils.toBean(pageResult, OaNoticeRespVO.class));
    }
    @GetMapping("/get")
    @Operation(summary = "获取通知公告详情")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('bpm:oa-notice:query')")
    public CommonResult<OaNoticeRespVO> getNotice(@RequestParam("id") Long id) {
        OaNoticeDO notice = oaNoticeService.getNotice(id, getLoginUserId());
        return success(BeanUtils.toBean(notice, OaNoticeRespVO.class));
    }
    @GetMapping("/my-unread-count")
    @Operation(summary = "获取我的未读通知公告数量")
    public CommonResult<Long> getMyUnreadCount() {
        return success(oaNoticeService.getUnreadCount(getLoginUserId()));
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/OaNoticePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Schema(description = "管理后台 - é€šçŸ¥å…¬å‘Šåˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class OaNoticePageReqVO extends PageParam {
    @Schema(description = "公告标题,模糊匹配", example = "放假")
    private String title;
    @Schema(description = "公告类型", example = "1")
    private Integer type;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "发送范围", example = "1")
    private Integer sendScope;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/OaNoticeRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - é€šçŸ¥å…¬å‘Š Response VO")
@Data
public class OaNoticeRespVO {
    @Schema(description = "通知公告编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "关于放假的通知")
    private String title;
    @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "根据国家规定...")
    private String content;
    @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer type;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer status;
    @Schema(description = "发送范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer sendScope;
    @Schema(description = "是否置顶", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer isTop;
    @Schema(description = "发布人ID", example = "1")
    private Long publishUserId;
    @Schema(description = "发布时间", example = "2025-01-01 10:00:00")
    private LocalDateTime publishTime;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
    @Schema(description = "指定用户ID列表(仅sendScope=2时有值)")
    private List<Long> userIds;
    @Schema(description = "已读人数", example = "10")
    private Long readCount;
    @Schema(description = "当前用户是否已读", example = "true")
    private Boolean isRead;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/oa/vo/OaNoticeSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.bpm.controller.admin.oa.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;
@Schema(description = "管理后台 - é€šçŸ¥å…¬å‘Šåˆ›å»º/修改 Request VO")
@Data
public class OaNoticeSaveReqVO {
    @Schema(description = "通知公告编号", example = "1024")
    private Long id;
    @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "关于放假的通知")
    @NotBlank(message = "公告标题不能为空")
    @Size(max = 50, message = "公告标题不能超过50个字符")
    private String title;
    @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "根据国家规定...")
    @NotBlank(message = "公告内容不能为空")
    private String content;
    @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "公告类型不能为空")
    private Integer type;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "发送范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "发送范围不能为空")
    private Integer sendScope;
    @Schema(description = "是否置顶", example = "0")
    private Integer isTop;
    @Schema(description = "指定用户ID列表(sendScope=2时必填)", example = "[1, 2, 3]")
    private List<Long> userIds;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/OaNoticeDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.oa;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.bpm.enums.oa.OaNoticeSendScopeEnum;
import cn.iocoder.yudao.module.bpm.enums.oa.OaNoticeStatusEnum;
import cn.iocoder.yudao.module.bpm.enums.oa.OaNoticeTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
 * OA é€šçŸ¥å…¬å‘Š DO
 */
@TableName("oa_notice")
@KeySequence("oa_notice_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class OaNoticeDO extends BaseDO {
    @TableId
    private Long id;
    /**
     * å…¬å‘Šæ ‡é¢˜
     */
    private String title;
    /**
     * å…¬å‘Šå†…容
     */
    private String content;
    /**
     * å…¬å‘Šç±»åž‹
     *
     * æžšä¸¾ {@link OaNoticeTypeEnum}
     */
    private Integer type;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link OaNoticeStatusEnum}
     */
    private Integer status;
    /**
     * å‘送范围
     *
     * æžšä¸¾ {@link OaNoticeSendScopeEnum}
     */
    private Integer sendScope;
    /**
     * æ˜¯å¦ç½®é¡¶
     */
    private Integer isTop;
    /**
     * å‘布人ID
     */
    private Long publishUserId;
    /**
     * å‘布时间
     */
    private LocalDateTime publishTime;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/OaNoticeReadDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.oa;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
 * OA é€šçŸ¥å…¬å‘Šå·²è¯»è®°å½• DO
 */
@TableName("oa_notice_read")
@KeySequence("oa_notice_read_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class OaNoticeReadDO extends BaseDO {
    @TableId
    private Long id;
    /**
     * é€šçŸ¥å…¬å‘ŠID
     */
    private Long noticeId;
    /**
     * ç”¨æˆ·ID
     */
    private Long userId;
    /**
     * é˜…读时间
     */
    private LocalDateTime readTime;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/oa/OaNoticeUserDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.oa;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * OA é€šçŸ¥å…¬å‘ŠæŒ‡å®šäººå…³è” DO
 */
@TableName("oa_notice_user")
@KeySequence("oa_notice_user_seq")
@Data
@EqualsAndHashCode(callSuper = true)
public class OaNoticeUserDO extends BaseDO {
    @TableId
    private Long id;
    /**
     * é€šçŸ¥å…¬å‘ŠID
     */
    private Long noticeId;
    /**
     * æŒ‡å®šç”¨æˆ·ID
     */
    private Long userId;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/OaNoticeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.oa;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticePageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Set;
@Mapper
public interface OaNoticeMapper extends BaseMapperX<OaNoticeDO> {
    default PageResult<OaNoticeDO> selectPage(OaNoticePageReqVO reqVO, Set<Long> noticeIds) {
        LambdaQueryWrapperX<OaNoticeDO> wrapper = new LambdaQueryWrapperX<OaNoticeDO>()
                .likeIfPresent(OaNoticeDO::getTitle, reqVO.getTitle())
                .eqIfPresent(OaNoticeDO::getType, reqVO.getType())
                .eqIfPresent(OaNoticeDO::getStatus, reqVO.getStatus())
                .eqIfPresent(OaNoticeDO::getSendScope, reqVO.getSendScope())
                .orderByDesc(OaNoticeDO::getIsTop)
                .orderByDesc(OaNoticeDO::getId);
        if (noticeIds != null) {
            wrapper.in(OaNoticeDO::getId, noticeIds);
        }
        return selectPage(reqVO, wrapper);
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/OaNoticeReadMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.oa;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeReadDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface OaNoticeReadMapper extends BaseMapperX<OaNoticeReadDO> {
    default OaNoticeReadDO selectByNoticeIdAndUserId(Long noticeId, Long userId) {
        return selectOne(new LambdaQueryWrapperX<OaNoticeReadDO>()
                .eq(OaNoticeReadDO::getNoticeId, noticeId)
                .eq(OaNoticeReadDO::getUserId, userId));
    }
    default List<OaNoticeReadDO> selectListByNoticeId(Long noticeId) {
        return selectList(new LambdaQueryWrapperX<OaNoticeReadDO>()
                .eq(OaNoticeReadDO::getNoticeId, noticeId));
    }
    default Long selectCountByNoticeId(Long noticeId) {
        return selectCount(new LambdaQueryWrapperX<OaNoticeReadDO>()
                .eq(OaNoticeReadDO::getNoticeId, noticeId));
    }
    default Long selectUnreadCountByUserId(Long userId, List<Long> noticeIds) {
        if (noticeIds == null || noticeIds.isEmpty()) {
            return 0L;
        }
        // æ€»å…¬å‘Šæ•° - å·²è¯»æ•° = æœªè¯»æ•°
        return (long) noticeIds.size() - selectCount(new LambdaQueryWrapperX<OaNoticeReadDO>()
                .in(OaNoticeReadDO::getNoticeId, noticeIds)
                .eq(OaNoticeReadDO::getUserId, userId));
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/oa/OaNoticeUserMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.oa;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeUserDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface OaNoticeUserMapper extends BaseMapperX<OaNoticeUserDO> {
    default List<OaNoticeUserDO> selectListByNoticeId(Long noticeId) {
        return selectList(new LambdaQueryWrapperX<OaNoticeUserDO>()
                .eq(OaNoticeUserDO::getNoticeId, noticeId));
    }
    default void deleteByNoticeId(Long noticeId) {
        delete(new LambdaQueryWrapperX<OaNoticeUserDO>()
                .eq(OaNoticeUserDO::getNoticeId, noticeId));
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
@@ -13,6 +13,7 @@
    // ========== OA æµç¨‹æ¨¡å— 1-009-001-000 ==========
    ErrorCode OA_LEAVE_NOT_EXISTS = new ErrorCode(1_009_001_001, "请假申请不存在");
    ErrorCode OA_NOTICE_NOT_EXISTS = new ErrorCode(1_009_001_002, "通知公告不存在");
    // ========== æµç¨‹æ¨¡åž‹ 1-009-002-000 ==========
    ErrorCode MODEL_KEY_EXISTS = new ErrorCode(1_009_002_000, "已经存在流程标识为【{}】的流程");
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/oa/OaNoticeSendScopeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.bpm.enums.oa;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * OA é€šçŸ¥å…¬å‘Šå‘送范围枚举
 */
@Getter
@AllArgsConstructor
public enum OaNoticeSendScopeEnum {
    ALL(1, "通知所有人"),
    SPECIFIED(2, "指定人");
    private final Integer scope;
    private final String name;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/oa/OaNoticeStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.bpm.enums.oa;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * OA é€šçŸ¥å…¬å‘ŠçŠ¶æ€æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum OaNoticeStatusEnum {
    DRAFT(0, "草稿"),
    PUBLISHED(1, "已发布"),
    REVOKED(2, "已撤销");
    private final Integer status;
    private final String name;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/oa/OaNoticeTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.bpm.enums.oa;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * OA é€šçŸ¥å…¬å‘Šç±»åž‹æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum OaNoticeTypeEnum {
    NOTIFICATION(1, "通知"),
    ANNOUNCEMENT(2, "公告");
    private final Integer type;
    private final String name;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/OaNoticeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.bpm.service.oa;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticePageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticeSaveReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeDO;
/**
 * OA é€šçŸ¥å…¬å‘Š Service æŽ¥å£
 */
public interface OaNoticeService {
    /**
     * åˆ›å»ºé€šçŸ¥å…¬å‘Š
     *
     * @param userId å½“前用户ID
     * @param createReqVO åˆ›å»ºè¯·æ±‚
     * @return é€šçŸ¥å…¬å‘ŠID
     */
    Long createNotice(Long userId, OaNoticeSaveReqVO createReqVO);
    /**
     * æ›´æ–°é€šçŸ¥å…¬å‘Š
     *
     * @param updateReqVO æ›´æ–°è¯·æ±‚
     */
    void updateNotice(OaNoticeSaveReqVO updateReqVO);
    /**
     * åˆ é™¤é€šçŸ¥å…¬å‘Š
     *
     * @param id ç¼–号
     */
    void deleteNotice(Long id);
    /**
     * èŽ·å¾—é€šçŸ¥å…¬å‘Šåˆ†é¡µåˆ—è¡¨ï¼ˆæŒ‰å¯è§æ€§è¿‡æ»¤ï¼‰
     *
     * @param userId å½“前用户ID
     * @param reqVO åˆ†é¡µæ¡ä»¶
     * @return åˆ†é¡µåˆ—表
     */
    PageResult<OaNoticeDO> getNoticePage(Long userId, OaNoticePageReqVO reqVO);
    /**
     * èŽ·å¾—é€šçŸ¥å…¬å‘Šè¯¦æƒ…ï¼ˆåŒæ—¶æ ‡è®°å·²è¯»ï¼‰
     *
     * @param id ç¼–号
     * @param userId å½“前用户ID
     * @return é€šçŸ¥å…¬å‘Š
     */
    OaNoticeDO getNotice(Long id, Long userId);
    /**
     * èŽ·å–å½“å‰ç”¨æˆ·æœªè¯»é€šçŸ¥å…¬å‘Šæ•°é‡
     *
     * @param userId å½“前用户ID
     * @return æœªè¯»æ•°é‡
     */
    Long getUnreadCount(Long userId);
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/oa/OaNoticeServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,256 @@
package cn.iocoder.yudao.module.bpm.service.oa;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticePageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.OaNoticeSaveReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeReadDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.OaNoticeUserDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.oa.OaNoticeMapper;
import cn.iocoder.yudao.module.bpm.dal.mysql.oa.OaNoticeReadMapper;
import cn.iocoder.yudao.module.bpm.dal.mysql.oa.OaNoticeUserMapper;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.bpm.enums.oa.OaNoticeSendScopeEnum;
import cn.iocoder.yudao.module.bpm.enums.oa.OaNoticeStatusEnum;
import cn.iocoder.yudao.module.system.dal.mysql.notify.NotifyMessageMapper;
import cn.iocoder.yudao.module.system.service.notify.NotifySendService;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.OA_NOTICE_NOT_EXISTS;
/**
 * OA é€šçŸ¥å…¬å‘Š Service å®žçŽ°ç±»
 */
@Service
@Slf4j
public class OaNoticeServiceImpl implements OaNoticeService {
    @Resource
    private OaNoticeMapper oaNoticeMapper;
    @Resource
    private OaNoticeUserMapper oaNoticeUserMapper;
    @Resource
    private OaNoticeReadMapper oaNoticeReadMapper;
    @Resource
    private NotifySendService notifySendService;
    @Resource
    private AdminUserService adminUserService;
    @Resource
    private NotifyMessageMapper notifyMessageMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createNotice(Long userId, OaNoticeSaveReqVO createReqVO) {
        // åˆ›å»ºé€šçŸ¥å…¬å‘Š
        OaNoticeDO notice = BeanUtils.toBean(createReqVO, OaNoticeDO.class);
        notice.setPublishUserId(userId);
        if (OaNoticeStatusEnum.PUBLISHED.getStatus().equals(createReqVO.getStatus())) {
            notice.setPublishTime(LocalDateTime.now());
        }
        oaNoticeMapper.insert(notice);
        // æŒ‡å®šäººå†™å…¥å…³è”表
        if (OaNoticeSendScopeEnum.SPECIFIED.getScope().equals(createReqVO.getSendScope())
                && CollUtil.isNotEmpty(createReqVO.getUserIds())) {
            saveNoticeUsers(notice.getId(), createReqVO.getUserIds());
        }
        // å·²å‘布则发送系统通知
        if (OaNoticeStatusEnum.PUBLISHED.getStatus().equals(notice.getStatus())) {
            sendPublishNotify(notice);
        }
        return notice.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateNotice(OaNoticeSaveReqVO updateReqVO) {
        // æ ¡éªŒæ˜¯å¦å­˜åœ¨
        OaNoticeDO oldNotice = validateNoticeExists(updateReqVO.getId());
        // æ›´æ–°é€šçŸ¥å…¬å‘Š
        OaNoticeDO updateObj = BeanUtils.toBean(updateReqVO, OaNoticeDO.class);
        // ä»Žæœªå‘布变为已发布时,设置发布时间
        if (!OaNoticeStatusEnum.PUBLISHED.getStatus().equals(oldNotice.getStatus())
                && OaNoticeStatusEnum.PUBLISHED.getStatus().equals(updateReqVO.getStatus())) {
            updateObj.setPublishTime(LocalDateTime.now());
        }
        oaNoticeMapper.updateById(updateObj);
        // æ›´æ–°æŒ‡å®šäººå…³è”
        oaNoticeUserMapper.deleteByNoticeId(updateReqVO.getId());
        if (OaNoticeSendScopeEnum.SPECIFIED.getScope().equals(updateReqVO.getSendScope())
                && CollUtil.isNotEmpty(updateReqVO.getUserIds())) {
            saveNoticeUsers(updateReqVO.getId(), updateReqVO.getUserIds());
        }
        // æ–°å‘布时发送系统通知
        if (!OaNoticeStatusEnum.PUBLISHED.getStatus().equals(oldNotice.getStatus())
                && OaNoticeStatusEnum.PUBLISHED.getStatus().equals(updateReqVO.getStatus())) {
            sendPublishNotify(updateObj);
        }
        // æ’¤é”€æ—¶åˆ é™¤å·²å‘送的系统通知
        if (OaNoticeStatusEnum.REVOKED.getStatus().equals(updateReqVO.getStatus())
                && OaNoticeStatusEnum.PUBLISHED.getStatus().equals(oldNotice.getStatus())) {
            notifyMessageMapper.deleteByTemplateCodeAndNoticeId(
                    "oa-notice-publish", updateReqVO.getId());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteNotice(Long id) {
        validateNoticeExists(id);
        oaNoticeMapper.deleteById(id);
        oaNoticeUserMapper.deleteByNoticeId(id);
    }
    @Override
    public PageResult<OaNoticeDO> getNoticePage(Long userId, OaNoticePageReqVO reqVO) {
        // æž„建可见的通知ID集合
        Set<Long> visibleIds = buildVisibleNoticeIds(userId);
        if (visibleIds != null && visibleIds.isEmpty()) {
            return PageResult.empty();
        }
        return oaNoticeMapper.selectPage(reqVO, visibleIds);
    }
    @Override
    public OaNoticeDO getNotice(Long id, Long userId) {
        OaNoticeDO notice = validateNoticeExists(id);
        // æ ‡è®°å·²è¯»
        markAsRead(id, userId);
        return notice;
    }
    @Override
    public Long getUnreadCount(Long userId) {
        // èŽ·å–æ‰€æœ‰å·²å‘å¸ƒä¸”å½“å‰ç”¨æˆ·å¯è§çš„å…¬å‘ŠID
        Set<Long> visibleIds = buildVisibleNoticeIds(userId);
        if (CollUtil.isEmpty(visibleIds)) {
            return 0L;
        }
        List<Long> visibleIdList = new ArrayList<>(visibleIds);
        return oaNoticeReadMapper.selectUnreadCountByUserId(userId, visibleIdList);
    }
    /**
     * æž„建当前用户可见的通知公告ID集合
     *
     * @param userId å½“前用户ID
     * @return null表示全部可见,空集合表示无可看
     */
    private Set<Long> buildVisibleNoticeIds(Long userId) {
        // æ‰€æœ‰é€šçŸ¥æ‰€æœ‰äººå…¬å‘ŠID
        List<OaNoticeDO> allPublishedNotices = oaNoticeMapper.selectList(
                new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<OaNoticeDO>()
                        .eq(OaNoticeDO::getSendScope, OaNoticeSendScopeEnum.ALL.getScope()));
        Set<Long> allNoticeIds = allPublishedNotices.stream()
                .map(OaNoticeDO::getId).collect(Collectors.toSet());
        // æŒ‡å®šäººèƒ½çœ‹åˆ°çš„æœ¬ç”¨æˆ·ç›¸å…³å…¬å‘ŠID
        List<OaNoticeDO> specifiedNotices = oaNoticeMapper.selectList(
                new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<OaNoticeDO>()
                        .eq(OaNoticeDO::getSendScope, OaNoticeSendScopeEnum.SPECIFIED.getScope()));
        for (OaNoticeDO notice : specifiedNotices) {
            // åˆ›å»ºè€…可见
            if (userId.equals(notice.getPublishUserId())) {
                allNoticeIds.add(notice.getId());
                continue;
            }
            // è¢«æŒ‡å®šç”¨æˆ·å¯è§
            Long count = oaNoticeUserMapper.selectCount(
                    new cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX<OaNoticeUserDO>()
                            .eq(OaNoticeUserDO::getNoticeId, notice.getId())
                            .eq(OaNoticeUserDO::getUserId, userId));
            if (count > 0) {
                allNoticeIds.add(notice.getId());
            }
        }
        return allNoticeIds;
    }
    /**
     * æ ‡è®°å·²è¯»
     */
    private void markAsRead(Long noticeId, Long userId) {
        OaNoticeReadDO existing = oaNoticeReadMapper.selectByNoticeIdAndUserId(noticeId, userId);
        if (existing == null) {
            OaNoticeReadDO readRecord = new OaNoticeReadDO();
            readRecord.setNoticeId(noticeId);
            readRecord.setUserId(userId);
            readRecord.setReadTime(LocalDateTime.now());
            oaNoticeReadMapper.insert(readRecord);
        }
    }
    /**
     * ä¿å­˜æŒ‡å®šäººå…³è”
     */
    private void saveNoticeUsers(Long noticeId, List<Long> userIds) {
        List<OaNoticeUserDO> userList = userIds.stream().map(uid -> {
            OaNoticeUserDO user = new OaNoticeUserDO();
            user.setNoticeId(noticeId);
            user.setUserId(uid);
            return user;
        }).collect(Collectors.toList());
        oaNoticeUserMapper.insertBatch(userList);
    }
    /**
     * å‘送发布通知给目标用户
     */
    private void sendPublishNotify(OaNoticeDO notice) {
        Map<String, Object> templateParams = new HashMap<>();
        templateParams.put("title", notice.getTitle());
        templateParams.put("content", notice.getContent());
        templateParams.put("noticeId", notice.getId());
        List<Long> targetUserIds;
        if (OaNoticeSendScopeEnum.ALL.getScope().equals(notice.getSendScope())) {
            // é€šçŸ¥æ‰€æœ‰äººï¼šæŸ¥è¯¢æ‰€æœ‰å¯ç”¨çš„管理员用户
            targetUserIds = adminUserService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus())
                    .stream().map(cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO::getId)
                    .collect(Collectors.toList());
        } else {
            // æŒ‡å®šäºº
            targetUserIds = oaNoticeUserMapper.selectListByNoticeId(notice.getId()).stream()
                    .map(OaNoticeUserDO::getUserId)
                    .collect(Collectors.toList());
        }
        for (Long userId : targetUserIds) {
            try {
                notifySendService.sendSingleNotifyToAdmin(userId, "oa-notice-publish", templateParams);
            } catch (Exception e) {
                log.warn("发送通知公告[{}]给用户[{}]失败", notice.getId(), userId, e);
            }
        }
    }
    private OaNoticeDO validateNoticeExists(Long id) {
        OaNoticeDO notice = oaNoticeMapper.selectById(id);
        if (notice == null) {
            throw exception(OA_NOTICE_NOT_EXISTS);
        }
        return notice;
    }
}
yudao-module-crm/pom.xml
@@ -99,5 +99,19 @@
            <version>2026.06-jdk25-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
        <!-- AI å¤§æ¨¡åž‹ API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-ai-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- PDF è§£æž -->
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>3.0.4</version>
        </dependency>
    </dependencies>
</project>
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/ai/CrmSaleQuotationAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.crm.controller.admin.quotation.ai;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo.CrmSaleQuotationOcrReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo.CrmSaleQuotationOcrRespVO;
import cn.iocoder.yudao.module.crm.service.quotation.ai.CrmSaleQuotationAiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - CRM é”€å”®æŠ¥ä»·å• AI OCR")
@RestController
@RequestMapping("/crm/sale-quotation/ai")
@Validated
public class CrmSaleQuotationAiController {
    @Resource
    private CrmSaleQuotationAiService saleQuotationAiService;
    @PostMapping("/ocr")
    @Operation(summary = "AI OCR è¯†åˆ«æŠ¥ä»·æ–‡ä»¶")
    @PreAuthorize("@ss.hasPermission('crm:sale-quotation:create')")
    public CommonResult<CrmSaleQuotationOcrRespVO> ocrQuotation(@Valid @RequestBody CrmSaleQuotationOcrReqVO reqVO) {
        return success(saleQuotationAiService.ocrQuotation(reqVO));
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/ai/vo/CrmSaleQuotationOcrReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI OCR è¯†åˆ«æŠ¥ä»·æ–‡ä»¶è¯·æ±‚ VO")
@Data
public class CrmSaleQuotationOcrReqVO {
    @Schema(description = "已上传文件的 blob ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "blob ID ä¸èƒ½ä¸ºç©º")
    private Long blobId;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/ai/vo/CrmSaleQuotationOcrRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - AI OCR è¯†åˆ«æŠ¥ä»·æ–‡ä»¶å“åº” VO")
@Data
public class CrmSaleQuotationOcrRespVO {
    @Schema(description = "报价单名称", example = "XX公司报价单")
    private String name;
    @Schema(description = "客户名称", example = "XX科技有限公司")
    private String customerName;
    @Schema(description = "报价日期", example = "2026-07-31")
    private LocalDate quotationTime;
    @Schema(description = "有效期至", example = "2026-08-30")
    private LocalDate validUntil;
    @Schema(description = "税率(百分比)", example = "13.0")
    private BigDecimal taxRate;
    @Schema(description = "折扣率(百分比)", example = "5.0")
    private BigDecimal discountPercent;
    @Schema(description = "备注", example = "含税含运费")
    private String remark;
    @Schema(description = "识别出的物料明细")
    private List<Item> items;
    @Schema(description = "原始识别文本(前端可选展示)")
    private String rawText;
    @Schema(description = "物料明细项")
    @Data
    public static class Item {
        @Schema(description = "物料名称", example = "伺服电机")
        private String itemName;
        @Schema(description = "物料规格", example = "SV-1000W")
        private String itemSpec;
        @Schema(description = "数量", example = "10")
        private BigDecimal count;
        @Schema(description = "单价", example = "1500.00")
        private BigDecimal quotationPrice;
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/ai/CrmSaleQuotationAiService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
package cn.iocoder.yudao.module.crm.service.quotation.ai;
import cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo.CrmSaleQuotationOcrReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo.CrmSaleQuotationOcrRespVO;
public interface CrmSaleQuotationAiService {
    CrmSaleQuotationOcrRespVO ocrQuotation(CrmSaleQuotationOcrReqVO reqVO);
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/ai/CrmSaleQuotationAiServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,281 @@
package cn.iocoder.yudao.module.crm.service.quotation.ai;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.ai.api.chat.AiChatApi;
import cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo.CrmSaleQuotationOcrReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.quotation.ai.vo.CrmSaleQuotationOcrRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO;
import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class CrmSaleQuotationAiServiceImpl implements CrmSaleQuotationAiService {
    private static final Set<String> SUPPORTED_EXTENSIONS = Set.of(
            "pdf", "doc", "docx", "xls", "xlsx", "png", "jpg", "jpeg", "gif", "bmp", "txt", "csv"
    );
    private static final Set<String> IMAGE_EXTENSIONS = Set.of("png", "jpg", "jpeg", "gif", "bmp");
    private static final String SYSTEM_PROMPT = """
            ä½ æ˜¯ä¸€ä½ä¸“业的销售单据数据提取专家。你需要从报价文件中提取结构化信息。
            ## æå–规则
            1. ä»”细识别文档中的报价单信息:客户名称、报价日期、有效期、物料明细等
            2. å¯¹äºŽç‰©æ–™æ˜Žç»†ï¼Œæ¯ä¸€è¡Œæå–:物料名称、规格型号、数量、单价
            3. ç¨ŽçŽ‡ã€æŠ˜æ‰£çŽ‡ç­‰å¦‚æ–‡ä»¶ä¸­æœªæ˜Žç¡®æ ‡æ³¨ï¼Œä¸å¡«
            4. æ—¥æœŸç»Ÿä¸€è½¬æ¢ä¸º yyyy-MM-dd æ ¼å¼
            5. æ•°å­—字段仅保留数字,去掉单位
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"name": "报价单名称", "customerName": "客户名称", "quotationTime": "2026-01-01", "validUntil": "2026-12-31", "taxRate": 13.0, "discountPercent": 5.0, "remark": "备注信息", "items": [{"itemName": "物料名称", "itemSpec": "规格型号", "count": 10.0, "quotationPrice": 100.0}]}
            æ‰€æœ‰å­—段均为可选,未提取到的字段不要返回。
            å¦‚果文件内容无法识别为报价单,返回空JSON:{}
            """;
    private static final String SYSTEM_PROMPT_IMAGE = """
            ä½ æ˜¯ä¸€ä½ä¸“业的销售单据OCR识别专家。你需要从报价文件图片中提取结构化信息。
            ## æå–规则
            1. ä»”细识别图片中的文字信息
            2. æå–报价单相关信息:客户名称、报价日期、有效期、物料明细等
            3. å¯¹äºŽç‰©æ–™æ˜Žç»†ï¼Œæ¯ä¸€è¡Œæå–:物料名称、规格型号、数量、单价
            4. ç¨ŽçŽ‡ã€æŠ˜æ‰£çŽ‡ç­‰å¦‚å›¾ç‰‡ä¸­æœªæ˜Žç¡®æ ‡æ³¨ï¼Œä¸å¡«
            5. æ—¥æœŸç»Ÿä¸€è½¬æ¢ä¸º yyyy-MM-dd æ ¼å¼
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"name": "报价单名称", "customerName": "客户名称", "quotationTime": "2026-01-01", "validUntil": "2026-12-31", "taxRate": 13.0, "discountPercent": 5.0, "remark": "备注信息", "items": [{"itemName": "物料名称", "itemSpec": "规格型号", "count": 10.0, "quotationPrice": 100.0}]}
            æ‰€æœ‰å­—段均为可选,未提取到的字段不要返回。
            å¦‚果图片内容无法识别为报价单,返回空JSON:{}
            """;
    @Resource
    private SystemStorageBlobService blobService;
    @Resource
    private AiChatApi aiChatApi;
    @Override
    public CrmSaleQuotationOcrRespVO ocrQuotation(CrmSaleQuotationOcrReqVO reqVO) {
        File tempFile = null;
        try {
            // 1. ä¸‹è½½æ–‡ä»¶
            SystemStorageBlobDO blob = blobService.getStorageBlob(reqVO.getBlobId());
            if (blob == null) {
                CrmSaleQuotationOcrRespVO error = new CrmSaleQuotationOcrRespVO();
                error.setRawText("文件不存在");
                return error;
            }
            // 2. æ ¡éªŒæ–‡ä»¶ç±»åž‹
            String ext = FileUtil.extName(blob.getOriginalFilename()).toLowerCase();
            if (!SUPPORTED_EXTENSIONS.contains(ext)) {
                CrmSaleQuotationOcrRespVO error = new CrmSaleQuotationOcrRespVO();
                error.setRawText("不支持的文件类型,请上传 PDF/Word/Excel/图片文件");
                return error;
            }
            // 3. èŽ·å–æ–‡ä»¶å­—èŠ‚
            tempFile = blobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey());
            if (tempFile == null || !tempFile.exists()) {
                CrmSaleQuotationOcrRespVO error = new CrmSaleQuotationOcrRespVO();
                error.setRawText("文件读取失败");
                return error;
            }
            byte[] fileBytes = Files.readAllBytes(tempFile.toPath());
            // 4. AI è¯†åˆ«ï¼ˆå›¾ç‰‡èµ°å¤šæ¨¡æ€ï¼Œæ–‡æœ¬èµ°çº¯æ–‡æœ¬ï¼‰
            boolean isImage = IMAGE_EXTENSIONS.contains(ext);
            String systemPrompt = isImage ? SYSTEM_PROMPT_IMAGE : SYSTEM_PROMPT;
            String aiResponse;
            if (isImage) {
                String imageBase64 = Base64.getEncoder().encodeToString(fileBytes);
                String mimeType = getMimeType(ext);
                aiResponse = aiChatApi.chatWithImage(systemPrompt, "请识别图片中的报价信息", imageBase64, mimeType);
            } else {
                String extractedText = extractTextByType(ext, fileBytes);
                if (StrUtil.isEmpty(extractedText)) {
                    CrmSaleQuotationOcrRespVO error = new CrmSaleQuotationOcrRespVO();
                    error.setRawText("文件内容为空,无法识别");
                    return error;
                }
                aiResponse = aiChatApi.chat(systemPrompt, extractedText);
            }
            log.info("AI OCR è¯†åˆ«ç»“æžœ: blobId={}, file={}, response={}",
                    reqVO.getBlobId(), blob.getOriginalFilename(), aiResponse);
            // 5. è§£æžå“åº”
            CrmSaleQuotationOcrRespVO result = parseOcrResponse(aiResponse);
            return result;
        } catch (Exception e) {
            log.warn("AI OCR è¯†åˆ«å¤±è´¥ï¼ŒblobId={}", reqVO.getBlobId(), e);
            CrmSaleQuotationOcrRespVO fallback = new CrmSaleQuotationOcrRespVO();
            fallback.setRawText("AI è¯†åˆ«æš‚时不可用,请手动录入");
            return fallback;
        } finally {
            if (tempFile != null && tempFile.exists()) {
                tempFile.delete();
            }
        }
    }
    private String extractTextByType(String ext, byte[] bytes) throws IOException {
        return switch (ext) {
            case "pdf" -> extractPdfText(bytes);
            case "docx", "doc" -> extractDocxText(bytes);
            case "xlsx", "xls" -> extractExcelText(bytes);
            case "txt", "csv" -> new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
            default -> "";
        };
    }
    private String extractPdfText(byte[] bytes) throws IOException {
        try (PDDocument doc = Loader.loadPDF(bytes)) {
            PDFTextStripper stripper = new PDFTextStripper();
            stripper.setSortByPosition(true);
            String text = stripper.getText(doc);
            return StrUtil.isNotBlank(text) ? text.trim() : "";
        }
    }
    private String extractDocxText(byte[] bytes) throws IOException {
        try (XWPFDocument doc = new XWPFDocument(new ByteArrayInputStream(bytes))) {
            try (XWPFWordExtractor extractor = new XWPFWordExtractor(doc)) {
                String text = extractor.getText();
                return StrUtil.isNotBlank(text) ? text.trim() : "";
            }
        }
    }
    private String extractExcelText(byte[] bytes) throws IOException {
        StringBuilder sb = new StringBuilder();
        try (Workbook wb = WorkbookFactory.create(new ByteArrayInputStream(bytes))) {
            for (int i = 0; i < wb.getNumberOfSheets(); i++) {
                Sheet sheet = wb.getSheetAt(i);
                if (i > 0) sb.append("\n--- Sheet: ").append(sheet.getSheetName()).append(" ---\n");
                for (Row row : sheet) {
                    List<String> cellValues = new ArrayList<>();
                    for (Cell cell : row) {
                        cellValues.add(getCellStringValue(cell));
                    }
                    sb.append(String.join("\t", cellValues)).append("\n");
                }
            }
        }
        return sb.toString().trim();
    }
    private String getCellStringValue(Cell cell) {
        return switch (cell.getCellType()) {
            case STRING -> cell.getStringCellValue().trim();
            case NUMERIC -> {
                if (DateUtil.isCellDateFormatted(cell)) {
                    yield cell.getLocalDateTimeCellValue().toLocalDate().toString();
                }
                double v = cell.getNumericCellValue();
                if (v == Math.floor(v) && !Double.isInfinite(v)) {
                    yield String.valueOf((long) v);
                }
                yield String.valueOf(v);
            }
            case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
            case FORMULA -> {
                try {
                    yield String.valueOf(cell.getNumericCellValue());
                } catch (Exception e) {
                    yield cell.getStringCellValue().trim();
                }
            }
            default -> "";
        };
    }
    private String getMimeType(String ext) {
        return switch (ext) {
            case "jpg", "jpeg" -> "image/jpeg";
            case "gif" -> "image/gif";
            case "bmp" -> "image/bmp";
            default -> "image/png";
        };
    }
    private CrmSaleQuotationOcrRespVO parseOcrResponse(String aiResponse) {
        CrmSaleQuotationOcrRespVO respVO = new CrmSaleQuotationOcrRespVO();
        try {
            String json = aiResponse;
            int start = json.indexOf("{");
            int end = json.lastIndexOf("}");
            if (start >= 0 && end > start) {
                json = json.substring(start, end + 1);
            }
            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
            respVO.setName((String) parsed.get("name"));
            respVO.setCustomerName((String) parsed.get("customerName"));
            respVO.setRemark((String) parsed.get("remark"));
            Object qt = parsed.get("quotationTime");
            if (qt instanceof String s && StrUtil.isNotBlank(s)) {
                try { respVO.setQuotationTime(LocalDate.parse(s)); } catch (Exception ignored) {}
            }
            Object vu = parsed.get("validUntil");
            if (vu instanceof String s && StrUtil.isNotBlank(s)) {
                try { respVO.setValidUntil(LocalDate.parse(s)); } catch (Exception ignored) {}
            }
            Object tr = parsed.get("taxRate");
            if (tr instanceof Number n) respVO.setTaxRate(BigDecimal.valueOf(n.doubleValue()));
            Object dp = parsed.get("discountPercent");
            if (dp instanceof Number n) respVO.setDiscountPercent(BigDecimal.valueOf(n.doubleValue()));
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> items = (List<Map<String, Object>>) parsed.get("items");
            if (CollUtil.isNotEmpty(items)) {
                respVO.setItems(items.stream().map(m -> {
                    CrmSaleQuotationOcrRespVO.Item item = new CrmSaleQuotationOcrRespVO.Item();
                    item.setItemName((String) m.get("itemName"));
                    item.setItemSpec((String) m.get("itemSpec"));
                    Object count = m.get("count");
                    if (count instanceof Number n) item.setCount(BigDecimal.valueOf(n.doubleValue()));
                    Object price = m.get("quotationPrice");
                    if (price instanceof Number n) item.setQuotationPrice(BigDecimal.valueOf(n.doubleValue()));
                    return item;
                }).collect(Collectors.toList()));
            }
        } catch (Exception e) {
            log.warn("解析 AI OCR å“åº”失败: {}", aiResponse, e);
            respVO.setRawText(aiResponse);
        }
        return respVO;
    }
    private String truncate(String text, int maxLen) {
        if (text == null) return "";
        return text.length() > maxLen ? text.substring(0, maxLen) + "..." : text;
    }
}
yudao-module-mes/pom.xml
@@ -66,6 +66,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- AI å¤§æ¨¡åž‹ API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-ai-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <!-- Web ç›¸å…³ -->
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/MesProAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo.*;
import cn.iocoder.yudao.module.mes.service.pro.ai.MesProAiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES ç”Ÿäº§ AI è¾…助分析")
@RestController
@RequestMapping("/mes/pro/ai")
public class MesProAiController {
    @Resource
    private MesProAiService proAiService;
    @PostMapping("/predict-material")
    @Operation(summary = "AI ç‰©æ–™çŸ­ç¼ºé¢„测")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<MesProAiMaterialPredictRespVO> predictMaterial(@Valid @RequestBody MesProAiMaterialPredictReqVO reqVO) {
        return success(proAiService.predictMaterial(reqVO));
    }
    @PostMapping("/predict-duration")
    @Operation(summary = "AI ç”Ÿäº§è®¡åˆ’时长预测")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<MesProAiDurationPredictRespVO> predictDuration(@Valid @RequestBody MesProAiDurationPredictReqVO reqVO) {
        return success(proAiService.predictDuration(reqVO));
    }
    @PostMapping("/predict-risk")
    @Operation(summary = "AI ç”Ÿäº§é£Žé™©é¢„测")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<MesProAiRiskPredictRespVO> predictRisk(@Valid @RequestBody MesProAiRiskPredictReqVO reqVO) {
        return success(proAiService.predictRisk(reqVO));
    }
    @PostMapping("/predict-delivery")
    @Operation(summary = "AI æ˜¯å¦èƒ½æŒ‰æ—¶äº¤ä»˜")
    @PreAuthorize("@ss.hasPermission('mes:pro-work-order:query')")
    public CommonResult<MesProAiDeliveryPredictRespVO> predictDelivery(@Valid @RequestBody MesProAiDeliveryPredictReqVO reqVO) {
        return success(proAiService.predictDelivery(reqVO));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDeliveryPredictReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI æŒ‰æ—¶äº¤ä»˜é¢„测请求 VO")
@Data
public class MesProAiDeliveryPredictReqVO {
    @Schema(description = "工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "工单 ID ä¸èƒ½ä¸ºç©º")
    private Long workOrderId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDeliveryPredictRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - AI æŒ‰æ—¶äº¤ä»˜é¢„测响应 VO")
@Data
public class MesProAiDeliveryPredictRespVO {
    @Schema(description = "是否能按时交付", example = "true")
    private Boolean onTime;
    @Schema(description = "置信度(0-100)", example = "85.5")
    private Double confidence;
    @Schema(description = "AI æŽ¨ç†åˆ†æž")
    private String reasoning;
    @Schema(description = "延期因素")
    private List<String> delayFactors;
    @Schema(description = "生产进度百分比", example = "60.0")
    private BigDecimal progressPercent;
    @Schema(description = "距离需求日期剩余天数", example = "10")
    private Integer remainingDays;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDurationPredictReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI ç”Ÿäº§æ—¶é•¿é¢„测请求 VO")
@Data
public class MesProAiDurationPredictReqVO {
    @Schema(description = "工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "工单 ID ä¸èƒ½ä¸ºç©º")
    private Long workOrderId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiDurationPredictRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - AI ç”Ÿäº§æ—¶é•¿é¢„测响应 VO")
@Data
public class MesProAiDurationPredictRespVO {
    @Schema(description = "预测总时长(小时)", example = "48.5")
    private BigDecimal predictedTotalHours;
    @Schema(description = "标准总时长(小时)", example = "40.0")
    private BigDecimal standardTotalHours;
    @Schema(description = "AI æŽ¨ç†åˆ†æž")
    private String reasoning;
    @Schema(description = "关键关注点")
    private List<String> keyPoints;
    @Schema(description = "各工序时长明细")
    private List<ProcessTimeBreakdown> processBreakdown;
    @Schema(description = "工序时长明细")
    @Data
    public static class ProcessTimeBreakdown {
        @Schema(description = "工序名称", example = "车削")
        private String processName;
        @Schema(description = "工序顺序", example = "1")
        private Integer sort;
        @Schema(description = "标准准备时间(分钟)", example = "30")
        private Integer standardPrepareTime;
        @Schema(description = "标准等待时间(分钟)", example = "15")
        private Integer standardWaitTime;
        @Schema(description = "标准生产时间(小时)", example = "8.0")
        private BigDecimal standardProductionTime;
        @Schema(description = "预测生产时间(小时)", example = "10.5")
        private BigDecimal predictedProductionTime;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiMaterialPredictReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI ç‰©æ–™çŸ­ç¼ºé¢„测请求 VO")
@Data
public class MesProAiMaterialPredictReqVO {
    @Schema(description = "工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "工单 ID ä¸èƒ½ä¸ºç©º")
    private Long workOrderId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiMaterialPredictRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - AI ç‰©æ–™çŸ­ç¼ºé¢„测响应 VO")
@Data
public class MesProAiMaterialPredictRespVO {
    @Schema(description = "整体风险等级:1=低风险 2=中风险 3=高风险", example = "2")
    private Integer riskLevel;
    @Schema(description = "AI ç»¼åˆåˆ†æž")
    private String reasoning;
    @Schema(description = "物料风险明细")
    private List<RiskItem> riskItems;
    @Schema(description = "物料风险项")
    @Data
    public static class RiskItem {
        @Schema(description = "物料编码", example = "MAT-001")
        private String itemCode;
        @Schema(description = "物料名称", example = "螺丝")
        private String itemName;
        @Schema(description = "需求量", example = "1000")
        private BigDecimal requiredQuantity;
        @Schema(description = "库存可用量", example = "500")
        private BigDecimal availableQuantity;
        @Schema(description = "短缺数量", example = "500")
        private BigDecimal shortageQuantity;
        @Schema(description = "风险等级:充足/ç´§å¼ /短缺", example = "短缺")
        private String riskLevel;
        @Schema(description = "建议措施", example = "建议紧急采购")
        private String suggestion;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiRiskPredictReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI ç”Ÿäº§é£Žé™©é¢„测请求 VO")
@Data
public class MesProAiRiskPredictReqVO {
    @Schema(description = "工单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "工单 ID ä¸èƒ½ä¸ºç©º")
    private Long workOrderId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/ai/vo/MesProAiRiskPredictRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - AI ç”Ÿäº§é£Žé™©é¢„测响应 VO")
@Data
public class MesProAiRiskPredictRespVO {
    @Schema(description = "整体风险等级:1=低风险 2=中风险 3=高风险", example = "2")
    private Integer overallRiskLevel;
    @Schema(description = "AI ç»¼åˆåˆ†æž")
    private String reasoning;
    @Schema(description = "关键关注点")
    private List<String> keyPoints;
    @Schema(description = "风险明细列表")
    private List<RiskItem> risks;
    @Schema(description = "风险项")
    @Data
    public static class RiskItem {
        @Schema(description = "风险类别:质量/设备/物料/工艺瓶颈", example = "设备")
        private String category;
        @Schema(description = "风险描述", example = "设备A上次保养距今已超过30天")
        private String description;
        @Schema(description = "严重程度:高/中/低", example = "中")
        private String severity;
        @Schema(description = "建议措施", example = "建议在开始生产前进行设备保养")
        private String suggestion;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ai/MesQcAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ai;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo.MesQcAiSuggestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo.MesQcAiSuggestRespVO;
import cn.iocoder.yudao.module.mes.service.qc.ai.MesQcAiService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES QC AI è¾…助判定")
@RestController
@RequestMapping("/mes/qc/ai")
public class MesQcAiController {
    @Resource
    private MesQcAiService qcAiService;
    @PostMapping("/suggest-result")
    @Operation(summary = "AI è¾…助判定检验结果")
    @PreAuthorize("@ss.hasPermission('mes:qc:update')")
    public CommonResult<MesQcAiSuggestRespVO> suggestCheckResult(@Valid @RequestBody MesQcAiSuggestReqVO reqVO) {
        return success(qcAiService.suggestCheckResult(reqVO));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ai/vo/MesQcAiSuggestReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - AI è¾…助判定请求 VO")
@Data
public class MesQcAiSuggestReqVO {
    @Schema(description = "质检单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "质检单 ID ä¸èƒ½ä¸ºç©º")
    private Long qcId;
    @Schema(description = "质检类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "质检类型不能为空")
    private Integer qcType;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ai/vo/MesQcAiSuggestRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - AI è¾…助判定响应 VO")
@Data
public class MesQcAiSuggestRespVO {
    @Schema(description = "建议的检验结果编码", example = "1")
    private Integer suggestedResult;
    @Schema(description = "建议的检验结果名称", example = "合格")
    private String suggestedResultName;
    @Schema(description = "AI æŽ¨ç†ä¾æ®")
    private String reasoning;
    @Schema(description = "关键关注点")
    private List<String> keyPoints;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/indicatorresult/MesQcIndicatorResultMapper.java
@@ -7,6 +7,8 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * MES æ£€éªŒç»“果记录 Mapper
 *
@@ -30,4 +32,10 @@
                .eq(MesQcIndicatorResultDO::getQcType, qcType));
    }
    default List<MesQcIndicatorResultDO> selectListByQcIdAndType(Long qcId, Integer qcType) {
        return selectList(new LambdaQueryWrapperX<MesQcIndicatorResultDO>()
                .eq(MesQcIndicatorResultDO::getQcId, qcId)
                .eq(MesQcIndicatorResultDO::getQcType, qcType));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.mes.service.pro.ai;
import cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo.*;
public interface MesProAiService {
    MesProAiMaterialPredictRespVO predictMaterial(MesProAiMaterialPredictReqVO reqVO);
    MesProAiDurationPredictRespVO predictDuration(MesProAiDurationPredictReqVO reqVO);
    MesProAiRiskPredictRespVO predictRisk(MesProAiRiskPredictReqVO reqVO);
    MesProAiDeliveryPredictRespVO predictDelivery(MesProAiDeliveryPredictReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/ai/MesProAiServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,782 @@
package cn.iocoder.yudao.module.mes.service.pro.ai;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.ai.api.chat.AiChatApi;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.ai.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationMachineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskIssueDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkstationMachineMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskIssueMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_WORK_ORDER_NOT_EXISTS;
@Slf4j
@Service
public class MesProAiServiceImpl implements MesProAiService {
    // ==================== System Prompts ====================
    private static final String SYSTEM_PROMPT_MATERIAL = """
            ä½ æ˜¯ä¸€ä½èµ„深的制造业物料管理专家,擅长分析生产工单的物料齐套性和短缺风险。
            ä½ éœ€è¦ç»¼åˆåˆ†æžä»¥ä¸‹ä¿¡æ¯ï¼Œç»™å‡ºä¸“业的物料预测建议:
            ## åˆ†æžç»´åº¦
            1. BOM éœ€æ±‚ vs åº“存可用量:对比每个物料的预计使用量与当前库存可用量
            2. åº“存可用量 = åœ¨åº“数量 - å†»ç»“数量 - å·²å ç”¨æ•°é‡
            3. å·²æŠ•料情况:已经发放到工位的物料是否充足
            4. é£Žé™©è¯„估:对有短缺风险的物料标注风险等级
            ## é£Žé™©ç­‰çº§å®šä¹‰
            - å……足:可用量 >= éœ€æ±‚量
            - ç´§å¼ ï¼šå¯ç”¨é‡ >= éœ€æ±‚量的50% ä½† < éœ€æ±‚量
            - çŸ­ç¼ºï¼šå¯ç”¨é‡ < éœ€æ±‚量的50% æˆ–可用量为0
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"riskLevel": 1, "reasoning": "综合分析...", "riskItems": [{"itemCode": "...", "itemName": "...", "requiredQuantity": 100.0, "availableQuantity": 50.0, "shortageQuantity": 50.0, "riskLevel": "ç´§å¼ ", "suggestion": "建议紧急采购或调拨"}]}
            riskLevel å–值:1=低风险 2=中风险 3=高风险
            """;
    private static final String SYSTEM_PROMPT_DURATION = """
            ä½ æ˜¯ä¸€ä½èµ„深的生产计划专家,擅长预测生产工单的实际生产时长。
            ä½ éœ€è¦ç»¼åˆåˆ†æžä»¥ä¸‹ä¿¡æ¯ï¼Œç»™å‡ºä¸“业的时长预测建议:
            ## åˆ†æžä¾æ®
            1. æ ‡å‡†å·¥æ—¶ï¼šå·¥è‰ºè·¯çº¿è®¾å®šçš„æ ‡å‡†å‡†å¤‡æ—¶é—´ã€ç­‰å¾…时间、生产时间
            2. è®¾å¤‡æ•ˆçŽ‡ï¼šå…³è”è®¾å¤‡çš„æ¯å°æ—¶äº§é‡ï¼Œä¼ é€å±¥å¸¦å€çŽ‡
            3. ç”Ÿäº§æ•°é‡ï¼šå·¥å•计划数量
            4. å®žé™…历史数据:已完成工序的实际生产时长
            5. è®¡ç®—公式:预测时长 = å‡†å¤‡æ—¶é—´ + ç­‰å¾…æ—¶é—´ + (计划数量 / (设备效率 * ä¼ é€å±¥å¸¦ç³»æ•°))(效率单位为每小时产量)
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"predictedTotalHours": 48.5, "standardTotalHours": 40.0, "reasoning": "...", "keyPoints": ["关键工序A是瓶颈"], "processBreakdown": [{"processName": "...", "sort": 1, "standardPrepareTime": 30, "standardWaitTime": 15, "standardProductionTime": 8.0, "predictedProductionTime": 10.5}]}
            predictedTotalHours å’Œ standardTotalHours ä¸ºå°æ•°
            """;
    private static final String SYSTEM_PROMPT_RISK = """
            ä½ æ˜¯ä¸€ä½èµ„深的制造风险管理专家,擅长识别生产过程中的各类风险。
            ä½ éœ€è¦ç»¼åˆåˆ†æžä»¥ä¸‹ä¿¡æ¯ï¼Œç»™å‡ºä¸“业的风险评估:
            ## åˆ†æžç»´åº¦
            1. è´¨é‡é£Žé™©ï¼šè¿‡ç¨‹æ£€éªŒ(IPQC)的缺陷率数据(致命/严重/轻微缺陷率)
            2. è®¾å¤‡é£Žé™©ï¼šè®¾å¤‡çŠ¶æ€ã€ä¸Šæ¬¡ä¿å…»æ—¶é—´ã€ä¸Šæ¬¡ç‚¹æ£€æ—¶é—´ã€ç”Ÿäº§æ•ˆçŽ‡
            3. ç‰©æ–™é£Žé™©ï¼šç‰©æ–™åº“存可用量与需求量的对比
            4. å·¥è‰ºç“¶é¢ˆï¼šå…³é”®å·¥åºçš„设备产能、任务排队情况
            ## é£Žé™©ç­‰çº§å®šä¹‰
            - é«˜ï¼šå­˜åœ¨è‡´å‘½ç¼ºé™·çއ>5% æˆ– è®¾å¤‡æ•…éšœ æˆ– å…³é”®ç‰©æ–™çŸ­ç¼º>50% æˆ– å…³é”®å·¥åºä¸¥é‡ç§¯åŽ‹
            - ä¸­ï¼šå­˜åœ¨ä¸¥é‡ç¼ºé™·çއ>10% æˆ– è®¾å¤‡é•¿æœŸæœªä¿å…» æˆ– ç‰©æ–™ç´§å¼ 
            - ä½Žï¼šå„项指标正常
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"overallRiskLevel": 2, "reasoning": "...", "keyPoints": ["关注点1"], "risks": [{"category": "设备", "description": "设备A上次保养距今已超30天", "severity": "中", "suggestion": "建议生产前保养"}]}
            overallRiskLevel å–值:1=低风险 2=中风险 3=高风险
            riskLevel å–值:充足/ç´§å¼ /短缺
            severity å–值:高/中/低
            category å–值:质量/设备/物料/工艺瓶颈
            """;
    private static final String SYSTEM_PROMPT_DELIVERY = """
            ä½ æ˜¯ä¸€ä½èµ„深的订单交付管理专家,擅长评估工单是否能按时交付。
            ä½ éœ€è¦ç»¼åˆåˆ†æžä»¥ä¸‹ä¿¡æ¯ï¼Œç»™å‡ºä¸“业的交付评估:
            ## åˆ†æžä¾æ®
            1. å½“前进度:已生产数量 / è®¡åˆ’数量的百分比
            2. å‰©ä½™æ—¶é—´ï¼šè·ç¦»éœ€æ±‚日期的天数(工作日按每天8小时计算)
            3. é¢„计剩余工时:根据工艺路线和设备效率预测的剩余生产时长
            4. ç”Ÿäº§é€Ÿåº¦ï¼šæ ¹æ®å·²å®Œæˆä»»åŠ¡çš„å®žé™…ç”Ÿäº§é€ŸçŽ‡
            5. ä»»åŠ¡çŠ¶æ€ï¼šå„å·¥åºçš„å®Œæˆæƒ…å†µ
            ## åˆ¤å®šé€»è¾‘
            - æŒ‰æ—¶äº¤ä»˜ï¼šå‰©ä½™å¤©æ•° * 8小时 >= é¢„计剩余工时,且进度正常
            - æœ‰å»¶æœŸé£Žé™©ï¼šå‰©ä½™å¤©æ•° * 8小时 >= é¢„计剩余工时的80%,但不足100%
            - æ— æ³•按时交付:剩余天数 * 8小时 < é¢„计剩余工时的80%,或关键工序卡阻
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"onTime": true, "confidence": 85.5, "reasoning": "...", "delayFactors": [], "progressPercent": 60.0, "remainingDays": 10}
            onTime: true=可按时交付 false=无法按时交付
            confidence: 0-100的数字,表示评估置信度
            """;
    // ==================== Dependencies ====================
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProWorkOrderBomMapper workOrderBomMapper;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProTaskMapper taskMapper;
    @Resource
    private MesProTaskIssueMapper taskIssueMapper;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @Resource
    private MesWmMaterialStockMapper materialStockMapper;
    @Resource
    private MesMdWorkstationMachineMapper workstationMachineMapper;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private CrmCustomerApi crmCustomerApi;
    @Resource
    private AiChatApi aiChatApi;
    // ==================== 1. ç‰©æ–™çŸ­ç¼ºé¢„测 ====================
    @Override
    public MesProAiMaterialPredictRespVO predictMaterial(MesProAiMaterialPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherMaterialData(reqVO.getWorkOrderId());
            String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_MATERIAL, JSONUtil.toJsonPrettyStr(data));
            log.info("AI ç‰©æ–™é¢„测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            return parseMaterialResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI ç‰©æ–™é¢„测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            MesProAiMaterialPredictRespVO fallback = new MesProAiMaterialPredictRespVO();
            fallback.setReasoning("AI åˆ†æžæš‚时不可用,请稍后重试");
            return fallback;
        }
    }
    private Map<String, Object> gatherMaterialData(Long workOrderId) {
        Map<String, Object> data = new LinkedHashMap<>();
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
        data.put("工单编号", workOrder.getCode());
        data.put("工单名称", workOrder.getName());
        data.put("计划数量", workOrder.getQuantity());
        data.put("已生产数量", workOrder.getQuantityProduced());
        fillItemInfo(data, workOrder.getProductId());
        // BOM ç‰©æ–™éœ€æ±‚
        List<MesProWorkOrderBomDO> bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(bomList)) {
            List<Map<String, Object>> materials = new ArrayList<>();
            for (MesProWorkOrderBomDO bom : bomList) {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("物料ID", bom.getItemId());
                String itemName = resolveItemName(bom.getItemId());
                item.put("物料名称", itemName);
                item.put("需求量", bom.getQuantity());
                // æŸ¥è¯¢åº“存可用量
                List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesWmMaterialStockDO>()
                                .eq(MesWmMaterialStockDO::getItemId, bom.getItemId())
                                .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO));
                BigDecimal available = BigDecimal.ZERO;
                if (CollUtil.isNotEmpty(stocks)) {
                    for (MesWmMaterialStockDO stock : stocks) {
                        BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO;
                        BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO;
                        available = available.add(stock.getQuantity().subtract(frozen).subtract(reserved));
                    }
                }
                item.put("库存可用量", available.compareTo(BigDecimal.ZERO) > 0 ? available : BigDecimal.ZERO);
                materials.add(item);
            }
            data.put("物料需求清单", materials);
        }
        // å·²æŠ•料情况
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(tasks)) {
            List<Map<String, Object>> issuedMaterials = new ArrayList<>();
            for (MesProTaskDO task : tasks) {
                List<MesProTaskIssueDO> issues = taskIssueMapper.selectListByTaskId(task.getId());
                for (MesProTaskIssueDO issue : issues) {
                    Map<String, Object> issued = new LinkedHashMap<>();
                    issued.put("物料ID", issue.getItemId());
                    issued.put("已投料数量", issue.getIssuedQuantity());
                    issued.put("已使用数量", issue.getUsedQuantity());
                    issued.put("可用数量", issue.getAvailableQuantity());
                    issuedMaterials.add(issued);
                }
            }
            if (!issuedMaterials.isEmpty()) {
                data.put("已投料记录", issuedMaterials);
            }
        }
        return data;
    }
    private MesProAiMaterialPredictRespVO parseMaterialResponse(String aiResponse) {
        MesProAiMaterialPredictRespVO respVO = new MesProAiMaterialPredictRespVO();
        try {
            String json = extractJson(aiResponse);
            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
            Object riskLevel = parsed.get("riskLevel");
            if (riskLevel instanceof Number num) respVO.setRiskLevel(num.intValue());
            respVO.setReasoning((String) parsed.get("reasoning"));
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> items = (List<Map<String, Object>>) parsed.get("riskItems");
            if (CollUtil.isNotEmpty(items)) {
                respVO.setRiskItems(items.stream().map(m -> {
                    MesProAiMaterialPredictRespVO.RiskItem ri = new MesProAiMaterialPredictRespVO.RiskItem();
                    ri.setItemCode((String) m.get("itemCode"));
                    ri.setItemName((String) m.get("itemName"));
                    ri.setRequiredQuantity(toBigDecimal(m.get("requiredQuantity")));
                    ri.setAvailableQuantity(toBigDecimal(m.get("availableQuantity")));
                    ri.setShortageQuantity(toBigDecimal(m.get("shortageQuantity")));
                    ri.setRiskLevel((String) m.get("riskLevel"));
                    ri.setSuggestion((String) m.get("suggestion"));
                    return ri;
                }).collect(Collectors.toList()));
            }
        } catch (Exception e) {
            log.warn("解析 AI ç‰©æ–™é¢„测响应失败: {}", aiResponse, e);
            respVO.setReasoning(aiResponse);
        }
        return respVO;
    }
    // ==================== 2. ç”Ÿäº§æ—¶é•¿é¢„测 ====================
    @Override
    public MesProAiDurationPredictRespVO predictDuration(MesProAiDurationPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherDurationData(reqVO.getWorkOrderId());
            String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_DURATION, JSONUtil.toJsonPrettyStr(data));
            log.info("AI æ—¶é•¿é¢„测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            return parseDurationResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI æ—¶é•¿é¢„测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            MesProAiDurationPredictRespVO fallback = new MesProAiDurationPredictRespVO();
            fallback.setReasoning("AI åˆ†æžæš‚时不可用,请稍后重试");
            return fallback;
        }
    }
    private Map<String, Object> gatherDurationData(Long workOrderId) {
        Map<String, Object> data = new LinkedHashMap<>();
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
        data.put("工单编号", workOrder.getCode());
        data.put("计划数量", workOrder.getQuantity());
        data.put("已生产数量", workOrder.getQuantityProduced());
        fillItemInfo(data, workOrder.getProductId());
        // å·¥åºä¿¡æ¯
        List<MesProWorkOrderProcessDO> processes = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId);
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
        Map<Long, MesProTaskDO> taskByProcessId = new HashMap<>();
        if (CollUtil.isNotEmpty(tasks)) {
            for (MesProTaskDO task : tasks) {
                if (task.getProcessId() != null) {
                    taskByProcessId.put(task.getProcessId(), task);
                }
            }
        }
        List<Map<String, Object>> processList = new ArrayList<>();
        BigDecimal standardTotalHours = BigDecimal.ZERO;
        for (MesProWorkOrderProcessDO proc : processes) {
            Map<String, Object> item = new LinkedHashMap<>();
            String processName = resolveProcessName(proc.getProcessId());
            item.put("工序名称", processName);
            item.put("顺序", proc.getSort());
            item.put("标准准备时间(分钟)", proc.getPrepareTime() != null ? proc.getPrepareTime() : 0);
            item.put("标准等待时间(分钟)", proc.getWaitTime() != null ? proc.getWaitTime() : 0);
            item.put("是否关键工序", proc.getKeyFlag() != null && proc.getKeyFlag());
            item.put("是否质检工序", proc.getCheckFlag() != null && proc.getCheckFlag());
            // è®¾å¤‡æ•ˆçއ
            MesProTaskDO task = taskByProcessId.get(proc.getProcessId());
            if (task != null && task.getWorkstationId() != null) {
                BigDecimal totalEfficiency = getWorkstationEfficiency(task.getWorkstationId());
                item.put("设备总效率(ä»¶/小时)", totalEfficiency);
                if (totalEfficiency.compareTo(BigDecimal.ZERO) > 0) {
                    BigDecimal productionHours = workOrder.getQuantity()
                            .subtract(workOrder.getQuantityProduced() != null ? workOrder.getQuantityProduced() : BigDecimal.ZERO)
                            .divide(totalEfficiency, 4, RoundingMode.HALF_UP);
                    item.put("预计生产时间(小时)", productionHours);
                }
                item.put("任务状态", task.getStatus());
                if (task.getDuration() != null) item.put("已用时长(小时)", task.getDuration());
            } else {
                item.put("设备总效率(ä»¶/小时)", "未排产无设备数据");
            }
            // æ ‡å‡†æ—¶é—´
            int prepareMin = proc.getPrepareTime() != null ? proc.getPrepareTime() : 0;
            int waitMin = proc.getWaitTime() != null ? proc.getWaitTime() : 0;
            BigDecimal standardHours = BigDecimal.valueOf(prepareMin + waitMin).divide(BigDecimal.valueOf(60), 4, RoundingMode.HALF_UP);
            standardTotalHours = standardTotalHours.add(standardHours);
            item.put("标准准备+等待时间(小时)", standardHours);
            processList.add(item);
        }
        data.put("工序明细", processList);
        data.put("标准总准备+等待时间(小时)", standardTotalHours);
        return data;
    }
    private BigDecimal getWorkstationEfficiency(Long workstationId) {
        BigDecimal totalEfficiency = BigDecimal.ZERO;
        List<MesMdWorkstationMachineDO> machines = workstationMachineMapper.selectListByWorkstationId(workstationId);
        if (CollUtil.isNotEmpty(machines)) {
            for (MesMdWorkstationMachineDO wm : machines) {
                MesDvMachineryDO machinery = machineryMapper.selectById(wm.getMachineryId());
                if (machinery != null && machinery.getProductionEfficiency() != null) {
                    BigDecimal efficiency = machinery.getProductionEfficiency();
                    if (machinery.getConveyorBelt() != null && machinery.getConveyorBelt() > 0) {
                        efficiency = efficiency.multiply(BigDecimal.valueOf(machinery.getConveyorBelt()));
                    }
                    totalEfficiency = totalEfficiency.add(efficiency);
                }
            }
        }
        return totalEfficiency;
    }
    private MesProAiDurationPredictRespVO parseDurationResponse(String aiResponse) {
        MesProAiDurationPredictRespVO respVO = new MesProAiDurationPredictRespVO();
        try {
            String json = extractJson(aiResponse);
            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
            respVO.setPredictedTotalHours(toBigDecimal(parsed.get("predictedTotalHours")));
            respVO.setStandardTotalHours(toBigDecimal(parsed.get("standardTotalHours")));
            respVO.setReasoning((String) parsed.get("reasoning"));
            @SuppressWarnings("unchecked")
            List<String> keyPoints = (List<String>) parsed.get("keyPoints");
            respVO.setKeyPoints(keyPoints);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> breakdown = (List<Map<String, Object>>) parsed.get("processBreakdown");
            if (CollUtil.isNotEmpty(breakdown)) {
                respVO.setProcessBreakdown(breakdown.stream().map(m -> {
                    MesProAiDurationPredictRespVO.ProcessTimeBreakdown pt = new MesProAiDurationPredictRespVO.ProcessTimeBreakdown();
                    pt.setProcessName((String) m.get("processName"));
                    pt.setSort(toInteger(m.get("sort")));
                    pt.setStandardPrepareTime(toInteger(m.get("standardPrepareTime")));
                    pt.setStandardWaitTime(toInteger(m.get("standardWaitTime")));
                    pt.setStandardProductionTime(toBigDecimal(m.get("standardProductionTime")));
                    pt.setPredictedProductionTime(toBigDecimal(m.get("predictedProductionTime")));
                    return pt;
                }).collect(Collectors.toList()));
            }
        } catch (Exception e) {
            log.warn("解析 AI æ—¶é•¿é¢„测响应失败: {}", aiResponse, e);
            respVO.setReasoning(aiResponse);
        }
        return respVO;
    }
    // ==================== 3. ç”Ÿäº§é£Žé™©é¢„测 ====================
    @Override
    public MesProAiRiskPredictRespVO predictRisk(MesProAiRiskPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherRiskData(reqVO.getWorkOrderId());
            String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_RISK, JSONUtil.toJsonPrettyStr(data));
            log.info("AI é£Žé™©é¢„测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            return parseRiskResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI é£Žé™©é¢„测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            MesProAiRiskPredictRespVO fallback = new MesProAiRiskPredictRespVO();
            fallback.setReasoning("AI åˆ†æžæš‚时不可用,请稍后重试");
            return fallback;
        }
    }
    private Map<String, Object> gatherRiskData(Long workOrderId) {
        Map<String, Object> data = new LinkedHashMap<>();
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
        data.put("工单编号", workOrder.getCode());
        data.put("工单名称", workOrder.getName());
        data.put("计划数量", workOrder.getQuantity());
        data.put("已生产数量", workOrder.getQuantityProduced());
        fillItemInfo(data, workOrder.getProductId());
        // è´¨é‡é£Žé™© - IPQC
        List<MesQcIpqcDO> ipqcList = ipqcMapper.selectList(
                new LambdaQueryWrapper<MesQcIpqcDO>()
                        .eq(MesQcIpqcDO::getWorkOrderId, workOrderId));
        if (CollUtil.isNotEmpty(ipqcList)) {
            List<Map<String, Object>> qcData = ipqcList.stream().map(ipqc -> {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("检验单编号", ipqc.getCode());
                item.put("致命缺陷率", ipqc.getCriticalRate() != null ? ipqc.getCriticalRate() + "%" : "0%");
                item.put("严重缺陷率", ipqc.getMajorRate() != null ? ipqc.getMajorRate() + "%" : "0%");
                item.put("轻微缺陷率", ipqc.getMinorRate() != null ? ipqc.getMinorRate() + "%" : "0%");
                item.put("致命缺陷数", ipqc.getCriticalQuantity() != null ? ipqc.getCriticalQuantity() : 0);
                item.put("严重缺陷数", ipqc.getMajorQuantity() != null ? ipqc.getMajorQuantity() : 0);
                item.put("轻微缺陷数", ipqc.getMinorQuantity() != null ? ipqc.getMinorQuantity() : 0);
                item.put("检验结果", ipqc.getCheckResult());
                return item;
            }).collect(Collectors.toList());
            data.put("过程检验(IPQC)记录", qcData);
        }
        // è®¾å¤‡é£Žé™©
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(tasks)) {
            Set<Long> machineryIds = new HashSet<>();
            for (MesProTaskDO task : tasks) {
                if (task.getWorkstationId() != null) {
                    List<MesMdWorkstationMachineDO> wms = workstationMachineMapper.selectListByWorkstationId(task.getWorkstationId());
                    for (MesMdWorkstationMachineDO wm : wms) {
                        machineryIds.add(wm.getMachineryId());
                    }
                }
            }
            if (!machineryIds.isEmpty()) {
                List<Map<String, Object>> equipmentData = new ArrayList<>();
                for (Long mid : machineryIds) {
                    MesDvMachineryDO machinery = machineryMapper.selectById(mid);
                    if (machinery != null) {
                        Map<String, Object> eq = new LinkedHashMap<>();
                        eq.put("设备编号", machinery.getCode());
                        eq.put("设备名称", machinery.getName());
                        eq.put("设备状态", machinery.getStatus());
                        eq.put("生产效率(ä»¶/小时)", machinery.getProductionEfficiency());
                        eq.put("传送履带倍数", machinery.getConveyorBelt());
                        eq.put("上次保养时间", machinery.getLastMaintenTime() != null
                                ? machinery.getLastMaintenTime().toString() : "无记录");
                        eq.put("上次点检时间", machinery.getLastCheckTime() != null
                                ? machinery.getLastCheckTime().toString() : "无记录");
                        equipmentData.add(eq);
                    }
                }
                data.put("关联设备信息", equipmentData);
            }
        }
        // ç‰©æ–™é£Žé™©ï¼ˆè½»é‡çº§ï¼šä»…标记可用量 < 20%的物料)
        List<MesProWorkOrderBomDO> bomList = workOrderBomMapper.selectListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(bomList)) {
            List<Map<String, Object>> materialRisks = new ArrayList<>();
            for (MesProWorkOrderBomDO bom : bomList) {
                List<MesWmMaterialStockDO> stocks = materialStockMapper.selectList(
                        new LambdaQueryWrapper<MesWmMaterialStockDO>()
                                .eq(MesWmMaterialStockDO::getItemId, bom.getItemId())
                                .ne(MesWmMaterialStockDO::getQuantity, BigDecimal.ZERO));
                BigDecimal available = BigDecimal.ZERO;
                if (CollUtil.isNotEmpty(stocks)) {
                    for (MesWmMaterialStockDO stock : stocks) {
                        BigDecimal frozen = stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO;
                        BigDecimal reserved = stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO;
                        available = available.add(stock.getQuantity().subtract(frozen).subtract(reserved));
                    }
                }
                if (bom.getQuantity() != null && bom.getQuantity().compareTo(BigDecimal.ZERO) > 0
                        && available.compareTo(bom.getQuantity().multiply(BigDecimal.valueOf(0.2))) < 0) {
                    Map<String, Object> risk = new LinkedHashMap<>();
                    risk.put("物料ID", bom.getItemId());
                    risk.put("物料名称", resolveItemName(bom.getItemId()));
                    risk.put("需求量", bom.getQuantity());
                    risk.put("可用量", available);
                    risk.put("缺口比例", available.compareTo(BigDecimal.ZERO) > 0
                            ? BigDecimal.ONE.subtract(available.divide(bom.getQuantity(), 2, RoundingMode.HALF_UP)).multiply(BigDecimal.valueOf(100)) + "%"
                            : "100%");
                    materialRisks.add(risk);
                }
            }
            if (!materialRisks.isEmpty()) {
                data.put("物料短缺风险", materialRisks);
            }
        }
        // å·¥è‰ºç“¶é¢ˆ - å…³é”®å·¥åºä¿¡æ¯
        List<MesProWorkOrderProcessDO> processes = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(processes)) {
            List<Map<String, Object>> keyProcesses = new ArrayList<>();
            for (MesProWorkOrderProcessDO proc : processes) {
                if (proc.getKeyFlag() != null && proc.getKeyFlag()) {
                    Map<String, Object> kp = new LinkedHashMap<>();
                    kp.put("工序名称", resolveProcessName(proc.getProcessId()));
                    kp.put("顺序", proc.getSort());
                    kp.put("标准准备时间(分钟)", proc.getPrepareTime());
                    kp.put("标准等待时间(分钟)", proc.getWaitTime());
                    keyProcesses.add(kp);
                }
            }
            if (!keyProcesses.isEmpty()) {
                data.put("关键工序", keyProcesses);
            }
        }
        return data;
    }
    private MesProAiRiskPredictRespVO parseRiskResponse(String aiResponse) {
        MesProAiRiskPredictRespVO respVO = new MesProAiRiskPredictRespVO();
        try {
            String json = extractJson(aiResponse);
            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
            Object riskLevel = parsed.get("overallRiskLevel");
            if (riskLevel instanceof Number num) respVO.setOverallRiskLevel(num.intValue());
            respVO.setReasoning((String) parsed.get("reasoning"));
            @SuppressWarnings("unchecked")
            List<String> keyPoints = (List<String>) parsed.get("keyPoints");
            respVO.setKeyPoints(keyPoints);
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> risks = (List<Map<String, Object>>) parsed.get("risks");
            if (CollUtil.isNotEmpty(risks)) {
                respVO.setRisks(risks.stream().map(m -> {
                    MesProAiRiskPredictRespVO.RiskItem ri = new MesProAiRiskPredictRespVO.RiskItem();
                    ri.setCategory((String) m.get("category"));
                    ri.setDescription((String) m.get("description"));
                    ri.setSeverity((String) m.get("severity"));
                    ri.setSuggestion((String) m.get("suggestion"));
                    return ri;
                }).collect(Collectors.toList()));
            }
        } catch (Exception e) {
            log.warn("解析 AI é£Žé™©é¢„测响应失败: {}", aiResponse, e);
            respVO.setReasoning(aiResponse);
        }
        return respVO;
    }
    // ==================== 4. æ˜¯å¦èƒ½æŒ‰æ—¶äº¤ä»˜ ====================
    @Override
    public MesProAiDeliveryPredictRespVO predictDelivery(MesProAiDeliveryPredictReqVO reqVO) {
        try {
            Map<String, Object> data = gatherDeliveryData(reqVO.getWorkOrderId());
            String aiResponse = aiChatApi.chat(SYSTEM_PROMPT_DELIVERY, JSONUtil.toJsonPrettyStr(data));
            log.info("AI äº¤ä»˜é¢„测: workOrderId={}, response={}", reqVO.getWorkOrderId(), aiResponse);
            return parseDeliveryResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI äº¤ä»˜é¢„测失败,workOrderId={}", reqVO.getWorkOrderId(), e);
            MesProAiDeliveryPredictRespVO fallback = new MesProAiDeliveryPredictRespVO();
            fallback.setReasoning("AI åˆ†æžæš‚时不可用,请稍后重试");
            return fallback;
        }
    }
    private Map<String, Object> gatherDeliveryData(Long workOrderId) {
        Map<String, Object> data = new LinkedHashMap<>();
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        if (workOrder == null) throw exception(PRO_WORK_ORDER_NOT_EXISTS);
        data.put("工单编号", workOrder.getCode());
        data.put("工单名称", workOrder.getName());
        data.put("计划数量", workOrder.getQuantity());
        data.put("已生产数量", workOrder.getQuantityProduced());
        data.put("工单状态", workOrder.getStatus());
        data.put("需求日期", workOrder.getRequestDate() != null ? workOrder.getRequestDate().toString() : "未设置");
        data.put("完成日期", workOrder.getFinishDate() != null ? workOrder.getFinishDate().toString() : "未完成");
        fillItemInfo(data, workOrder.getProductId());
        // è¿›åº¦è®¡ç®—
        BigDecimal progressPercent = BigDecimal.ZERO;
        if (workOrder.getQuantity() != null && workOrder.getQuantity().compareTo(BigDecimal.ZERO) > 0
                && workOrder.getQuantityProduced() != null) {
            progressPercent = workOrder.getQuantityProduced()
                    .divide(workOrder.getQuantity(), 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100));
        }
        data.put("生产进度(%)", progressPercent.setScale(1, RoundingMode.HALF_UP));
        // å‰©ä½™å¤©æ•°
        int remainingDays = 0;
        if (workOrder.getRequestDate() != null) {
            remainingDays = (int) ChronoUnit.DAYS.between(LocalDate.now(), workOrder.getRequestDate().toLocalDate());
        }
        data.put("距离需求日期剩余天数", Math.max(0, remainingDays));
        // ä»»åŠ¡å®Œæˆæƒ…å†µ
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(tasks)) {
            List<Map<String, Object>> taskData = new ArrayList<>();
            long finishedTasks = 0;
            long totalDuration = 0;
            for (MesProTaskDO task : tasks) {
                Map<String, Object> t = new LinkedHashMap<>();
                t.put("任务编号", task.getCode());
                t.put("任务名称", task.getName());
                t.put("任务状态", task.getStatus());
                t.put("计划数量", task.getQuantity());
                t.put("已生产数量", task.getProducedQuantity());
                t.put("实际时长(小时)", task.getDuration());
                if (task.getStartTime() != null) t.put("开始时间", task.getStartTime().toString());
                if (task.getEndTime() != null) t.put("结束时间", task.getEndTime().toString());
                if (task.getLastFeedbackTime() != null)
                    t.put("最后报工时间", task.getLastFeedbackTime().toString());
                taskData.add(t);
                if (task.getStatus() != null && task.getStatus() >= 3) finishedTasks++;
                if (task.getDuration() != null) totalDuration += task.getDuration();
            }
            data.put("任务列表", taskData);
            data.put("已完成任务数", finishedTasks);
            data.put("总任务数", tasks.size());
            data.put("已完成总时长(小时)", totalDuration);
        }
        // MPS å’Œå®¢æˆ·ä¿¡æ¯
        MesProMpsDO mps = mpsMapper.selectOne(
                new LambdaQueryWrapper<MesProMpsDO>()
                        .eq(MesProMpsDO::getWorkOrderId, workOrderId));
        if (mps != null) {
            data.put("MPS ç¼–号", mps.getCode());
            data.put("销售订单号", mps.getSaleOrderNo());
            data.put("MPS éœ€æ±‚日期", mps.getRequestDate() != null ? mps.getRequestDate().toString() : "");
            data.put("MPS ä¸‹å‘日期", mps.getIssueDate() != null ? mps.getIssueDate().toString() : "");
            fillClientInfo(data, mps.getClientId());
        }
        return data;
    }
    private MesProAiDeliveryPredictRespVO parseDeliveryResponse(String aiResponse) {
        MesProAiDeliveryPredictRespVO respVO = new MesProAiDeliveryPredictRespVO();
        try {
            String json = extractJson(aiResponse);
            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
            Object onTime = parsed.get("onTime");
            if (onTime instanceof Boolean b) respVO.setOnTime(b);
            Object confidence = parsed.get("confidence");
            if (confidence instanceof Number num) respVO.setConfidence(num.doubleValue());
            respVO.setReasoning((String) parsed.get("reasoning"));
            @SuppressWarnings("unchecked")
            List<String> delayFactors = (List<String>) parsed.get("delayFactors");
            respVO.setDelayFactors(delayFactors);
            respVO.setProgressPercent(toBigDecimal(parsed.get("progressPercent")));
            respVO.setRemainingDays(toInteger(parsed.get("remainingDays")));
        } catch (Exception e) {
            log.warn("解析 AI äº¤ä»˜é¢„测响应失败: {}", aiResponse, e);
            respVO.setReasoning(aiResponse);
        }
        return respVO;
    }
    // ==================== Shared Utilities ====================
    private String extractJson(String aiResponse) {
        int start = aiResponse.indexOf("{");
        int end = aiResponse.lastIndexOf("}");
        if (start >= 0 && end > start) {
            return aiResponse.substring(start, end + 1);
        }
        return aiResponse;
    }
    private BigDecimal toBigDecimal(Object value) {
        if (value == null) return BigDecimal.ZERO;
        if (value instanceof BigDecimal v) return v;
        if (value instanceof Number n) return BigDecimal.valueOf(n.doubleValue());
        try {
            return new BigDecimal(value.toString());
        } catch (Exception e) {
            return BigDecimal.ZERO;
        }
    }
    private Integer toInteger(Object value) {
        if (value == null) return 0;
        if (value instanceof Integer v) return v;
        if (value instanceof Number n) return n.intValue();
        try {
            return Integer.valueOf(value.toString());
        } catch (Exception e) {
            return 0;
        }
    }
    private String resolveItemName(Long itemId) {
        if (itemId == null) return "";
        try {
            MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
            if (item != null && StrUtil.isNotBlank(item.getName())) return item.getName();
        } catch (Exception ignored) {}
        return "物料" + itemId;
    }
    private String resolveProcessName(Long processId) {
        if (processId == null) return "";
        try {
            MesProProcessDO process = processService.getProcess(processId);
            if (process != null && StrUtil.isNotBlank(process.getName())) return process.getName();
        } catch (Exception ignored) {}
        return "工序" + processId;
    }
    private void fillItemInfo(Map<String, Object> data, Long itemId) {
        if (itemId == null) return;
        try {
            MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
            if (item != null) {
                data.put("物料编码", item.getCode() != null ? item.getCode() : itemId);
                data.put("物料名称", item.getName() != null ? item.getName() : "");
            }
        } catch (Exception e) {
            data.put("物料ID", itemId);
        }
    }
    private void fillClientInfo(Map<String, Object> data, Long clientId) {
        if (clientId == null) return;
        try {
            CrmCustomerRespDTO customer = crmCustomerApi.getCustomer(clientId);
            if (customer != null) {
                data.put("客户名称", customer.getName() != null ? customer.getName() : "");
            }
        } catch (Exception e) {
            data.put("客户ID", clientId);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ai/MesQcAiService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.service.qc.ai;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo.MesQcAiSuggestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo.MesQcAiSuggestRespVO;
/**
 * MES QC AI è¾…助判定 Service æŽ¥å£
 */
public interface MesQcAiService {
    /**
     * AI è¾…助判定检验结果
     *
     * @param reqVO è¯·æ±‚参数
     * @return AI åˆ¤å®šå»ºè®®
     */
    MesQcAiSuggestRespVO suggestCheckResult(MesQcAiSuggestReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ai/MesQcAiServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
package cn.iocoder.yudao.module.mes.service.qc.ai;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.ai.api.chat.AiChatApi;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo.MesQcAiSuggestReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ai.vo.MesQcAiSuggestRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.oqc.MesQcOqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.rqc.MesQcRqcDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicatorresult.MesQcIndicatorResultMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicatorresult.MesQcIndicatorResultDetailMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.defectrecord.MesQcDefectRecordMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum;
import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@Service
@Slf4j
public class MesQcAiServiceImpl implements MesQcAiService {
    private static final String SYSTEM_PROMPT = """
            ä½ æ˜¯ä¸€ä½èµ„深的质检专家,擅长根据检验数据判定产品是否合格。
            ä½ éœ€è¦ç»¼åˆåˆ†æžä»¥ä¸‹ä¿¡æ¯ï¼Œç»™å‡ºä¸“业的判定建议:
            ## åˆ¤å®šè§„则
            1. åˆæ ¼ï¼ˆç¼–码1):所有指标均在阈值范围内,无致命/严重缺陷
            2. ç‰¹é‡‡ï¼ˆç¼–码2):个别指标轻微超差,可让步接收
            3. ä¸åˆæ ¼é€€è´§ï¼ˆç¼–码3):多项指标超差或存在严重缺陷,不可接收
            4. ä¸åˆæ ¼æŠ¥åºŸï¼ˆç¼–码4):存在致命缺陷或多项严重超差,无法使用
            ## è¾“出格式
            è¯·æŒ‰ä»¥ä¸‹JSON格式返回(不要包含其他内容):
            {"suggestedResult": 1, "reasoning": "判定理由...", "keyPoints": ["关注点1", "关注点2"]}
            suggestedResult å–值:1合格 2特采 3不合格退货 4不合格报废
            reasoning:简明扼要的判定理由
            keyPoints:需要重点关注的指标或缺陷清单""";
    @Resource
    private MesQcIqcService iqcService;
    @Resource
    private MesQcIpqcService ipqcService;
    @Resource
    private MesQcOqcService oqcService;
    @Resource
    private MesQcRqcService rqcService;
    @Resource
    private MesQcIndicatorResultMapper indicatorResultMapper;
    @Resource
    private MesQcIndicatorResultDetailMapper indicatorResultDetailMapper;
    @Resource
    private MesQcDefectRecordMapper defectRecordMapper;
    @Resource
    private MesQcIndicatorService indicatorService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private CrmCustomerApi crmCustomerApi;
    @Resource
    private AiChatApi aiChatApi;
    @Override
    public MesQcAiSuggestRespVO suggestCheckResult(MesQcAiSuggestReqVO reqVO) {
        try {
            // 1. æ”¶é›†è´¨æ£€æ•°æ®
            Map<String, Object> qcData = gatherQcData(reqVO.getQcId(), reqVO.getQcType());
            // 2. æž„建 Prompt å¹¶è°ƒç”¨ AI
            String userMessage = JSONUtil.toJsonPrettyStr(qcData);
            String aiResponse = aiChatApi.chat(SYSTEM_PROMPT, userMessage);
            log.info("AI åˆ¤å®šç»“æžœ: qcId={}, qcType={}, response={}",
                    reqVO.getQcId(), reqVO.getQcType(), aiResponse);
            // 3. è§£æž AI è¿”回
            return parseAiResponse(aiResponse);
        } catch (Exception e) {
            log.warn("AI è¾…助判定失败,qcId={}, qcType={}", reqVO.getQcId(), reqVO.getQcType(), e);
            MesQcAiSuggestRespVO fallback = new MesQcAiSuggestRespVO();
            fallback.setReasoning("AI åˆ¤å®šæš‚时不可用,请人工判定");
            return fallback;
        }
    }
    private Map<String, Object> gatherQcData(Long qcId, Integer qcType) {
        Map<String, Object> data = new LinkedHashMap<>();
        // è´¨æ£€å•头信息
        if (MesBizTypeConstants.QC_IQC == qcType) {
            data.put("质检类型", "来料检验");
            MesQcIqcDO iqc = iqcService.getIqc(qcId);
            if (iqc == null) throw exception(QC_IQC_NOT_EXISTS);
            data.put("检验单编号", iqc.getCode());
            data.put("接收数量", iqc.getReceivedQuantity());
            data.put("检测数量", iqc.getCheckQuantity());
            data.put("合格数量", iqc.getQualifiedQuantity());
            data.put("不合格数量", iqc.getUnqualifiedQuantity());
            data.put("缺陷统计", Map.of(
                    "致命缺陷率", iqc.getCriticalRate() != null ? iqc.getCriticalRate() + "%" : "0%",
                    "严重缺陷率", iqc.getMajorRate() != null ? iqc.getMajorRate() + "%" : "0%",
                    "轻微缺陷率", iqc.getMinorRate() != null ? iqc.getMinorRate() + "%" : "0%",
                    "致命缺陷数", iqc.getCriticalQuantity() != null ? iqc.getCriticalQuantity() : 0,
                    "严重缺陷数", iqc.getMajorQuantity() != null ? iqc.getMajorQuantity() : 0,
                    "轻微缺陷数", iqc.getMinorQuantity() != null ? iqc.getMinorQuantity() : 0
            ));
            fillItemInfo(data, iqc.getItemId());
            fillVendorInfo(data, iqc.getVendorId());
        } else if (MesBizTypeConstants.QC_IPQC == qcType) {
            data.put("质检类型", "过程检验");
            MesQcIpqcDO ipqc = ipqcService.getIpqc(qcId);
            if (ipqc == null) throw exception(QC_IPQC_NOT_EXISTS);
            data.put("检验单编号", ipqc.getCode());
            data.put("检测数量", ipqc.getCheckQuantity());
            data.put("合格数量", ipqc.getQualifiedQuantity());
            data.put("不合格数量", ipqc.getUnqualifiedQuantity());
            data.put("工单ID", ipqc.getWorkOrderId());
            data.put("工序ID", ipqc.getProcessId());
            fillItemInfo(data, ipqc.getItemId());
        } else if (MesBizTypeConstants.QC_OQC == qcType) {
            data.put("质检类型", "出货检验");
            MesQcOqcDO oqc = oqcService.getOqc(qcId);
            if (oqc == null) throw exception(QC_OQC_NOT_EXISTS);
            data.put("检验单编号", oqc.getCode());
            data.put("出货数量", oqc.getOutQuantity());
            data.put("检测数量", oqc.getCheckQuantity());
            data.put("合格数量", oqc.getQualifiedQuantity());
            data.put("不合格数量", oqc.getUnqualifiedQuantity());
            data.put("最小检测数量", oqc.getMinCheckQuantity());
            data.put("最大不合格数量", oqc.getMaxUnqualifiedQuantity());
            fillItemInfo(data, oqc.getItemId());
            fillClientInfo(data, oqc.getClientId());
        } else if (MesBizTypeConstants.QC_RQC == qcType) {
            data.put("质检类型", "退货检验");
            MesQcRqcDO rqc = rqcService.getRqc(qcId);
            if (rqc == null) throw exception(QC_RQC_NOT_EXISTS);
            data.put("检验单编号", rqc.getCode());
            data.put("检测数量", rqc.getCheckQuantity());
            data.put("合格数量", rqc.getQualifiedQuantity());
            data.put("不合格数量", rqc.getUnqualifiedQuantity());
            fillItemInfo(data, rqc.getItemId());
        }
        // æŒ‡æ ‡æ£€éªŒç»“æžœ
        List<MesQcIndicatorResultDO> results = indicatorResultMapper.selectListByQcIdAndType(qcId, qcType);
        if (CollUtil.isNotEmpty(results)) {
            List<Map<String, Object>> indicatorData = new ArrayList<>();
            for (MesQcIndicatorResultDO result : results) {
                List<MesQcIndicatorResultDetailDO> details =
                        indicatorResultDetailMapper.selectListByResultId(result.getId());
                if (CollUtil.isEmpty(details)) continue;
                Map<String, Object> sample = new LinkedHashMap<>();
                sample.put("样品编号", result.getCode());
                sample.put("SN", result.getSn() != null ? result.getSn() : "");
                List<Map<String, String>> values = new ArrayList<>();
                for (MesQcIndicatorResultDetailDO detail : details) {
                    Map<String, String> item = new LinkedHashMap<>();
                    String indicatorName = indicatorService.getIndicator(detail.getIndicatorId()).getName();
                    item.put("指标", indicatorName);
                    item.put("实测值", detail.getValue());
                    values.add(item);
                }
                sample.put("检测值", values);
                indicatorData.add(sample);
            }
            data.put("检验指标结果", indicatorData);
        }
        // ç¼ºé™·è®°å½•
        List<MesQcDefectRecordDO> defects = defectRecordMapper.selectListByQcTypeAndQcId(qcType, qcId);
        if (CollUtil.isNotEmpty(defects)) {
            List<Map<String, Object>> defectData = defects.stream().map(d -> {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("缺陷名称", d.getName());
                item.put("缺陷等级", d.getLevel());
                item.put("缺陷数量", d.getQuantity());
                item.put("备注", d.getRemark() != null ? d.getRemark() : "");
                return item;
            }).collect(Collectors.toList());
            data.put("缺陷记录", defectData);
        }
        return data;
    }
    private void fillItemInfo(Map<String, Object> data, Long itemId) {
        if (itemId == null) return;
        try {
            MdmItemRespDTO item = mdmItemApi.getItem(itemId).getCheckedData();
            if (item != null) {
                data.put("物料编码", item.getCode() != null ? item.getCode() : itemId);
                data.put("物料名称", item.getName() != null ? item.getName() : "");
            }
        } catch (Exception e) {
            data.put("物料ID", itemId);
        }
    }
    private void fillVendorInfo(Map<String, Object> data, Long vendorId) {
        if (vendorId == null) return;
        try {
            SrmSupplierRespDTO vendor = srmSupplierApi.getSupplier(vendorId).getCheckedData();
            if (vendor != null) {
                data.put("供应商名称", vendor.getName() != null ? vendor.getName() : "");
            }
        } catch (Exception e) {
            data.put("供应商ID", vendorId);
        }
    }
    private void fillClientInfo(Map<String, Object> data, Long clientId) {
        if (clientId == null) return;
        try {
            CrmCustomerRespDTO customer = crmCustomerApi.getCustomer(clientId);
            if (customer != null) {
                data.put("客户名称", customer.getName() != null ? customer.getName() : "");
            }
        } catch (Exception e) {
            data.put("客户ID", clientId);
        }
    }
    private MesQcAiSuggestRespVO parseAiResponse(String aiResponse) {
        MesQcAiSuggestRespVO respVO = new MesQcAiSuggestRespVO();
        try {
            // æå– JSON éƒ¨åˆ†ï¼ˆAI å¯èƒ½åŒ…裹在 markdown ä»£ç å—中)
            String json = aiResponse;
            int start = json.indexOf("{");
            int end = json.lastIndexOf("}");
            if (start >= 0 && end > start) {
                json = json.substring(start, end + 1);
            }
            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
            Object result = parsed.get("suggestedResult");
            if (result instanceof Number num) {
                respVO.setSuggestedResult(num.intValue());
                // æ˜ å°„结果名称
                for (MesQcCheckResultEnum e : MesQcCheckResultEnum.values()) {
                    if (e.getType().equals(num.intValue())) {
                        respVO.setSuggestedResultName(e.getName());
                        break;
                    }
                }
            }
            respVO.setReasoning((String) parsed.get("reasoning"));
            @SuppressWarnings("unchecked")
            List<String> keyPoints = (List<String>) parsed.get("keyPoints");
            respVO.setKeyPoints(keyPoints);
        } catch (Exception e) {
            log.warn("解析 AI å“åº”失败,返回原始内容: {}", aiResponse, e);
            respVO.setReasoning(aiResponse);
        }
        return respVO;
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/notify/NotifyMessageMapper.java
@@ -67,4 +67,10 @@
                .eq(NotifyMessageDO::getUserType, userType));
    }
    default int deleteByTemplateCodeAndNoticeId(String templateCode, Long noticeId) {
        return delete(new QueryWrapperX<NotifyMessageDO>()
                .eq("template_code", templateCode)
                .apply("JSON_EXTRACT(template_params, '$.noticeId') = {0}", noticeId));
    }
}
yudao-server/pom.xml
@@ -102,11 +102,11 @@
        </dependency>
        <!-- AI å¤§æ¨¡åž‹ç›¸å…³æ¨¡å—。默认注释,保证编译速度 -->
<!--        <dependency>-->
<!--            <groupId>cn.iocoder.boot</groupId>-->
<!--            <artifactId>yudao-module-ai</artifactId>-->
<!--            <version>${revision}</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-ai</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- IoT ç‰©è”网相关模块。默认注释,保证编译速度 -->
<!--        <dependency>-->
yudao-server/src/main/resources/application-dev.yaml
@@ -11,17 +11,6 @@
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # TODO èŠ‹è‰¿ï¼šspring-ai-alibaba 2.0.0-M1.1 çš„ DashScope è‡ªåŠ¨é…ç½®æš‚ä¸å…¼å®¹ Spring AI 2.0.0
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAgentAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeVideoAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioSpeechAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioTranscriptionAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeRerankAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeMultimodalEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAsyncToolCallingManagerAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
yudao-server/src/main/resources/application-local.yaml
@@ -11,17 +11,22 @@
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # TODO èŠ‹è‰¿ï¼šspring-ai-alibaba 2.0.0-M1.1 çš„ DashScope è‡ªåŠ¨é…ç½®æš‚ä¸å…¼å®¹ Spring AI 2.0.0
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAgentAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeVideoAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioSpeechAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioTranscriptionAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeRerankAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeMultimodalEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAsyncToolCallingManagerAutoConfiguration
      # ç¦ç”¨æœªé…ç½® API Key çš„æ¨¡åž‹è‡ªåŠ¨é…ç½®ï¼ˆåªä¿ç•™é€šä¹‰åƒé—® Embedding çš„æ‰‹åŠ¨é…ç½®ï¼‰
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration
      - org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration
      - org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaApiAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration
      - org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
@@ -213,6 +218,8 @@
    cn.iocoder.yudao.module.im.dal.mysql: debug
    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO èŠ‹è‰¿ï¼šå…ˆç¦ç”¨ï¼ŒSpring Boot 3.X å­˜åœ¨éƒ¨åˆ†é”™è¯¯çš„ WARN æç¤º
--- #################### AI ç›¸å…³é…ç½® ####################
debug: false
--- #################### å¾®ä¿¡å…¬ä¼—号、小程序相关配置 ####################
yudao-server/src/main/resources/application-test.yaml
@@ -11,17 +11,6 @@
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # TODO èŠ‹è‰¿ï¼šspring-ai-alibaba 2.0.0-M1.1 çš„ DashScope è‡ªåŠ¨é…ç½®æš‚ä¸å…¼å®¹ Spring AI 2.0.0
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAgentAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeVideoAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioSpeechAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAudioTranscriptionAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeRerankAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeMultimodalEmbeddingAutoConfiguration
      - com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAsyncToolCallingManagerAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
yudao-server/src/main/resources/application.yaml
@@ -151,34 +151,10 @@
        database-name: default # Milvus ä¸­æ•°æ®åº“的名称
        collection-name: knowledge_segment # Milvus ä¸­é›†åˆçš„名称:用于存储向量数据的集合标识符,所有相关的向量操作都会在这个集合中进行
        client:
          host: 127.0.0.1
          host: 36.213.180.217
          port: 19530
    openai: # OpenAI å®˜æ–¹
      base-url: ${OPENAI_BASE_URL}
      api-key: ${OPENAI_API_KEY}
    anthropic: # Anthropic Claude
      base-url: ${ANTHROPIC_BASE_URL}
      api-key: ${ANTHROPIC_API_KEY}
    ollama:
      base-url: http://127.0.0.1:11434
      chat:
        model: llama3
    stabilityai:
      api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
    dashscope: # é€šä¹‰åƒé—®
      api-key: ${DASHSCOPE_API_KEY}
    deepseek: # DeepSeek
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        options:
          model: deepseek-v4-flash
    google:
      genai: # è°·æ­Œ Gemini
        api-key: ${GEMINI_API_KEY}
        chat:
          model: gemini-2.5-flash
    model:
      rerank: false # æ˜¯å¦å¼€å¯â€œé€šä¹‰åƒé—®â€çš„ Rerank æ¨¡åž‹ï¼Œå¡«å†™ dashscope å¼€å¯
      rerank: false # æ˜¯å¦å¼€å¯ Rerank æ¨¡åž‹
    mcp:
      server:
        enabled: false
@@ -199,60 +175,6 @@
yudao:
  ai:
    doubao: # å­—节豆包
      enable: true
      api-key: ${DOUBAO_API_KEY}
      model: doubao-seed-2-1-turbo-260628
    hunyuan: # è…¾è®¯æ··å…ƒ
      enable: true
      api-key: ${HUNYUAN_API_KEY}
      model: hy3-preview
    siliconflow: # ç¡…基流动
      enable: true
      api-key: ${SILICONFLOW_API_KEY}
      model: deepseek-ai/DeepSeek-V4-Pro
    xinghuo: # è®¯é£žæ˜Ÿç«
      enable: true
      api-key: ${XINGHUO_API_KEY}
      model: x2-flash
    baichuan: # ç™¾å·æ™ºèƒ½
      enable: true
      api-key: ${BAICHUAN_API_KEY}
      model: Baichuan-M3
    yiyan: # æ–‡å¿ƒä¸€è¨€
      enable: false
      api-key: ${YIYAN_API_KEY}
      model: ernie-5.1
    zhipu: # æ™ºè°± AI(GLM)
      enable: false
      api-key: ${ZHIPU_API_KEY}
      model: glm-5.2
    minimax: # MiniMax
      enable: false
      api-key: ${MINIMAX_API_KEY}
      model: MiniMax-M3
    moonshot: # æœˆä¹‹æš—面(KIMI)
      enable: false
      api-key: ${MOONSHOT_API_KEY}
      model: kimi-k2.6
    stepfun: # é˜¶è·ƒæ˜Ÿè¾°
      enable: false
      api-key: ${STEPFUN_API_KEY}
      model: step-3.7-flash
    grok: # Grok
      enable: false
      api-key: ${GROK_API_KEY}
      model: grok-4.3
    midjourney:
      enable: true
  #    base-url: https://api.holdai.top/mj-relax/mj
      base-url: https://api.holdai.top/mj
      api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf
      notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
    suno:
      enable: true
  #    base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
      base-url: http://127.0.0.1:3001
    web-search:
      enable: true
      api-key: sk-40500e52840f4d24b956d0b1d80d9abe